mcp-mesh 0.8.0b9__tar.gz → 0.9.0b1__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 (87) hide show
  1. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/.gitignore +18 -1
  2. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/PKG-INFO +4 -2
  3. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/__init__.py +1 -1
  4. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/dependency_injector.py +9 -0
  5. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/mesh_llm_agent.py +53 -24
  6. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/mesh_llm_agent_injector.py +210 -39
  7. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/provider_handlers/__init__.py +2 -0
  8. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/provider_handlers/base_provider_handler.py +177 -0
  9. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/provider_handlers/claude_handler.py +129 -102
  10. mcp_mesh-0.9.0b1/_mcp_mesh/engine/provider_handlers/gemini_handler.py +351 -0
  11. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/provider_handlers/generic_handler.py +31 -0
  12. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/provider_handlers/openai_handler.py +4 -1
  13. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +5 -1
  14. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +30 -1
  15. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/tracing/trace_context_helper.py +5 -3
  16. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/mesh/decorators.py +174 -92
  17. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/mesh/helpers.py +47 -2
  18. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/pyproject.toml +7 -5
  19. mcp_mesh-0.8.0b9/_mcp_mesh/engine/provider_handlers/gemini_handler.py +0 -181
  20. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/LICENSE +0 -0
  21. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/README.md +0 -0
  22. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/__init__.py +0 -0
  23. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/async_mcp_client.py +0 -0
  24. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/base_injector.py +0 -0
  25. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/decorator_registry.py +0 -0
  26. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/http_wrapper.py +0 -0
  27. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/llm_config.py +0 -0
  28. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/llm_errors.py +0 -0
  29. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/provider_handlers/provider_handler_registry.py +0 -0
  30. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/response_parser.py +0 -0
  31. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/self_dependency_proxy.py +0 -0
  32. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/session_aware_client.py +0 -0
  33. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/session_manager.py +0 -0
  34. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/signature_analyzer.py +0 -0
  35. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/tool_executor.py +0 -0
  36. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/tool_schema_builder.py +0 -0
  37. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/engine/unified_mcp_proxy.py +0 -0
  38. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/__init__.py +0 -0
  39. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_heartbeat/__init__.py +0 -0
  40. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +0 -0
  41. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_startup/__init__.py +0 -0
  42. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_startup/api_pipeline.py +0 -0
  43. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_startup/api_server_setup.py +0 -0
  44. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_startup/fastapi_discovery.py +0 -0
  45. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_startup/middleware_integration.py +0 -0
  46. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_startup/route_collection.py +0 -0
  47. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/api_startup/route_integration.py +0 -0
  48. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_heartbeat/__init__.py +0 -0
  49. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/__init__.py +0 -0
  50. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/configuration.py +0 -0
  51. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/decorator_collection.py +0 -0
  52. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +0 -0
  53. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +0 -0
  54. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +0 -0
  55. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +0 -0
  56. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/lifespan_factory.py +0 -0
  57. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/server_discovery.py +0 -0
  58. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +0 -0
  59. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +0 -0
  60. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/shared/__init__.py +0 -0
  61. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/shared/base_step.py +0 -0
  62. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -0
  63. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/pipeline/shared/pipeline_types.py +0 -0
  64. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/reload.py +0 -0
  65. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/reload_runner.py +0 -0
  66. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/__init__.py +0 -0
  67. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/config_resolver.py +0 -0
  68. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/content_extractor.py +0 -0
  69. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/defaults.py +0 -0
  70. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/fast_heartbeat_status.py +0 -0
  71. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/fastapi_middleware_manager.py +0 -0
  72. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/health_check_manager.py +0 -0
  73. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/host_resolver.py +0 -0
  74. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/logging_config.py +0 -0
  75. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/server_discovery.py +0 -0
  76. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/simple_shutdown.py +0 -0
  77. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/sse_parser.py +0 -0
  78. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/shared/support_types.py +0 -0
  79. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/tracing/agent_context_helper.py +0 -0
  80. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/tracing/context.py +0 -0
  81. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/tracing/execution_tracer.py +0 -0
  82. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/tracing/fastapi_tracing_middleware.py +0 -0
  83. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/tracing/redis_metadata_publisher.py +0 -0
  84. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/tracing/utils.py +0 -0
  85. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/_mcp_mesh/utils/fastmcp_schema_extractor.py +0 -0
  86. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/mesh/__init__.py +0 -0
  87. {mcp_mesh-0.8.0b9 → mcp_mesh-0.9.0b1}/mesh/types.py +0 -0
@@ -120,7 +120,7 @@ celerybeat.pid
120
120
  *.sage.py
121
121
 
122
122
  # Environments
123
- .env
123
+ **/.env
124
124
  .venv
125
125
  env/
126
126
  venv/
@@ -163,6 +163,12 @@ cython_debug/
163
163
  # VS Code
164
164
  .vscode/
165
165
 
166
+ # Eclipse / Maven IDE files
167
+ .classpath
168
+ .project
169
+ .settings/
170
+ .factorypath
171
+
166
172
  # Other IDEs/Tools
167
173
  .emigo_repomap/
168
174
  .windsurf/
@@ -175,6 +181,7 @@ logs/
175
181
  .logs/
176
182
  capability_store/
177
183
  prompts/
184
+ !examples/**/prompts/
178
185
  *.db
179
186
  *.pid
180
187
 
@@ -255,4 +262,14 @@ rust-core-implementation.org
255
262
  src/runtime/core/index.js
256
263
  src/runtime/core/index.d.ts
257
264
  test/
265
+ !src/runtime/java/**/src/test/
258
266
  scaffold-test/
267
+
268
+ # Test suite build outputs
269
+ tests/src-tests/out/
270
+ tests/lib-tests/out/
271
+
272
+ # Allow test suite build artifacts
273
+ !tests/**/build/
274
+ .claude/
275
+ *.org
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-mesh
3
- Version: 0.8.0b9
3
+ Version: 0.9.0b1
4
4
  Summary: Kubernetes-native platform for distributed MCP applications
5
5
  Project-URL: Homepage, https://github.com/dhyansraj/mcp-mesh
6
6
  Project-URL: Documentation, https://github.com/dhyansraj/mcp-mesh/tree/main/docs
@@ -18,6 +18,8 @@ Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
21
23
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
25
  Classifier: Topic :: System :: Distributed Computing
@@ -30,7 +32,7 @@ Requires-Dist: fastmcp<3.0.0,>=2.8.0
30
32
  Requires-Dist: httpx<1.0.0,>=0.25.0
31
33
  Requires-Dist: jinja2>=3.1.0
32
34
  Requires-Dist: litellm>=1.30.0
33
- Requires-Dist: mcp-mesh-core>=0.8.0b9
35
+ Requires-Dist: mcp-mesh-core>=0.9.0b1
34
36
  Requires-Dist: mcp<2.0.0,>=1.9.0
35
37
  Requires-Dist: prometheus-client<1.0.0,>=0.19.0
36
38
  Requires-Dist: pydantic<3.0.0,>=2.4.0
@@ -31,7 +31,7 @@ from .engine.decorator_registry import (
31
31
  get_decorator_stats,
32
32
  )
33
33
 
34
- __version__ = "0.8.0b9"
34
+ __version__ = "0.9.0b1"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -348,6 +348,15 @@ class DependencyInjector:
348
348
  logger.debug(f"🤖 Creating LLM injection wrapper for {function_id}")
349
349
  return self._llm_injector.create_injection_wrapper(func, function_id)
350
350
 
351
+ def initialize_direct_llm_agents(self) -> None:
352
+ """
353
+ Initialize LLM agents that use direct LiteLLM (no mesh delegation).
354
+
355
+ This should be called during agent startup to initialize agents that
356
+ don't need to wait for registry response.
357
+ """
358
+ self._llm_injector.initialize_direct_llm_agents()
359
+
351
360
  def create_injection_wrapper(
352
361
  self, func: Callable, dependencies: list[str]
353
362
  ) -> Callable:
@@ -9,13 +9,17 @@ import json
9
9
  import logging
10
10
  import time
11
11
  from pathlib import Path
12
- from typing import Any, Dict, List, Literal, Optional, Union
12
+ from typing import Any, Literal, Optional, Union
13
13
 
14
14
  from pydantic import BaseModel
15
15
 
16
16
  from .llm_config import LLMConfig
17
- from .llm_errors import (LLMAPIError, MaxIterationsError, ResponseParseError,
18
- ToolExecutionError)
17
+ from .llm_errors import (
18
+ LLMAPIError,
19
+ MaxIterationsError,
20
+ ResponseParseError,
21
+ ToolExecutionError,
22
+ )
19
23
  from .provider_handlers import ProviderHandlerRegistry
20
24
  from .response_parser import ResponseParser
21
25
  from .tool_executor import ToolExecutor
@@ -23,8 +27,7 @@ from .tool_schema_builder import ToolSchemaBuilder
23
27
 
24
28
  # Import Jinja2 for template rendering
25
29
  try:
26
- from jinja2 import (Environment, FileSystemLoader, Template,
27
- TemplateSyntaxError)
30
+ from jinja2 import Environment, FileSystemLoader, Template, TemplateSyntaxError
28
31
  except ImportError:
29
32
  Environment = None
30
33
  FileSystemLoader = None
@@ -536,8 +539,8 @@ IMPORTANT TOOL CALLING RULES:
536
539
 
537
540
  logger.debug(
538
541
  f"📥 Received response from mesh provider: "
539
- f"content={message_dict.get('content', '')[:200]}..., "
540
- f"tool_calls={len(message_dict.get('tool_calls', []))}"
542
+ f"content={(message_dict.get('content') or '')[:200]}..., "
543
+ f"tool_calls={len(message_dict.get('tool_calls') or [])}"
541
544
  )
542
545
 
543
546
  return MockResponse(message_dict)
@@ -615,13 +618,20 @@ IMPORTANT TOOL CALLING RULES:
615
618
  # Render base system prompt (from template or literal) with effective context
616
619
  base_system_prompt = self._render_system_prompt(effective_context)
617
620
 
618
- # Phase 2: Use provider handler to format system prompt
619
- # This allows vendor-specific optimizations (e.g., OpenAI skips JSON instructions)
620
- system_content = self._provider_handler.format_system_prompt(
621
- base_prompt=base_system_prompt,
622
- tool_schemas=self._tool_schemas,
623
- output_type=self.output_type,
624
- )
621
+ # Phase 2: Format system prompt
622
+ if self._is_mesh_delegated:
623
+ # Delegate path: Just use base prompt + basic tool instructions
624
+ # Provider will add vendor-specific formatting
625
+ system_content = base_system_prompt
626
+ if self._tool_schemas:
627
+ system_content += "\n\nYou have access to tools. Use them when needed to gather information."
628
+ else:
629
+ # Direct path: Use vendor handler for vendor-specific optimizations
630
+ system_content = self._provider_handler.format_system_prompt(
631
+ base_prompt=base_system_prompt,
632
+ tool_schemas=self._tool_schemas,
633
+ output_type=self.output_type,
634
+ )
625
635
 
626
636
  # Debug: Log system prompt (truncated for privacy)
627
637
  logger.debug(
@@ -633,12 +643,14 @@ IMPORTANT TOOL CALLING RULES:
633
643
  # Multi-turn conversation - use provided messages array
634
644
  messages = message.copy()
635
645
 
636
- # Ensure system prompt is prepended if not already present
637
- if not messages or messages[0].get("role") != "system":
638
- messages.insert(0, {"role": "system", "content": system_content})
639
- else:
640
- # Replace existing system message with our constructed one
641
- messages[0] = {"role": "system", "content": system_content}
646
+ # Only add/update system message if we have non-empty content
647
+ # (Claude API rejects empty system messages - though decorator provides default)
648
+ if system_content:
649
+ if not messages or messages[0].get("role") != "system":
650
+ messages.insert(0, {"role": "system", "content": system_content})
651
+ else:
652
+ # Replace existing system message with our constructed one
653
+ messages[0] = {"role": "system", "content": system_content}
642
654
 
643
655
  # Log conversation history
644
656
  logger.info(
@@ -646,10 +658,17 @@ IMPORTANT TOOL CALLING RULES:
646
658
  )
647
659
  else:
648
660
  # Single-turn - build messages array from string
649
- messages = [
650
- {"role": "system", "content": system_content},
651
- {"role": "user", "content": message},
652
- ]
661
+ # Only include system message if non-empty (Claude API rejects empty system messages)
662
+ if system_content:
663
+ messages = [
664
+ {"role": "system", "content": system_content},
665
+ {"role": "user", "content": message},
666
+ ]
667
+ else:
668
+ # Fallback for edge case where system_content is explicitly empty
669
+ messages = [
670
+ {"role": "user", "content": message},
671
+ ]
653
672
 
654
673
  logger.info(f"🚀 Starting agentic loop for message: {message[:100]}...")
655
674
 
@@ -705,6 +724,16 @@ IMPORTANT TOOL CALLING RULES:
705
724
  if self.model:
706
725
  model_params["model"] = self.model
707
726
 
727
+ # Issue #459: Include output_schema for provider to apply vendor-specific handling
728
+ # (e.g., OpenAI needs response_format, not prompt-based JSON instructions)
729
+ if self.output_type is not str and hasattr(
730
+ self.output_type, "model_json_schema"
731
+ ):
732
+ model_params["output_schema"] = (
733
+ self.output_type.model_json_schema()
734
+ )
735
+ model_params["output_type_name"] = self.output_type.__name__
736
+
708
737
  logger.debug(
709
738
  f"📤 Delegating to mesh provider with handler-prepared params: "
710
739
  f"keys={list(model_params.keys())}"
@@ -65,6 +65,78 @@ class MeshLlmAgentInjector(BaseInjector):
65
65
  super().__init__()
66
66
  self._llm_agents: dict[str, dict[str, Any]] = {}
67
67
 
68
+ def initialize_direct_llm_agents(self) -> None:
69
+ """
70
+ Initialize LLM agents that use direct LiteLLM (no mesh delegation).
71
+
72
+ This handles the case where:
73
+ - provider is a string (e.g., "claude") - direct LiteLLM call
74
+ - filter is None or empty - no mesh tools needed
75
+
76
+ These agents don't need to wait for registry response since all
77
+ information is available at decorator time.
78
+ """
79
+ llm_agents = DecoratorRegistry.get_mesh_llm_agents()
80
+
81
+ for function_id, llm_metadata in llm_agents.items():
82
+ config = llm_metadata.config
83
+ provider = config.get("provider")
84
+ filter_config = config.get("filter")
85
+
86
+ # Check if this is a direct LiteLLM agent (provider is string, not dict)
87
+ is_direct_llm = isinstance(provider, str)
88
+
89
+ # Check if no tools needed (filter is None or empty)
90
+ has_no_filter = filter_config is None or (
91
+ isinstance(filter_config, list) and len(filter_config) == 0
92
+ )
93
+
94
+ if is_direct_llm and has_no_filter:
95
+ # Skip if already initialized
96
+ if function_id in self._llm_agents:
97
+ continue
98
+
99
+ logger.info(
100
+ f"🔧 Initializing direct LiteLLM agent for '{function_id}' "
101
+ f"(provider={provider}, no filter)"
102
+ )
103
+
104
+ # Initialize empty tools data for direct LiteLLM
105
+ self._llm_agents[function_id] = {
106
+ "config": config,
107
+ "output_type": llm_metadata.output_type,
108
+ "param_name": llm_metadata.param_name,
109
+ "tools_metadata": [], # No tools for direct LiteLLM
110
+ "tools_proxies": {}, # No tool proxies needed
111
+ "function": llm_metadata.function,
112
+ "provider_proxy": None, # No mesh delegation
113
+ }
114
+
115
+ # Get the wrapper and update it with LLM agent
116
+ wrapper = llm_metadata.function
117
+ if wrapper and hasattr(wrapper, "_mesh_update_llm_agent"):
118
+ llm_agent = self._create_llm_agent(function_id)
119
+ wrapper._mesh_update_llm_agent(llm_agent)
120
+ logger.info(
121
+ f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}' (direct LiteLLM mode)"
122
+ )
123
+
124
+ # Set factory for per-call context agent creation (template support)
125
+ if config.get("is_template", False):
126
+
127
+ def create_context_agent(
128
+ context_value: Any, _func_id: str = function_id
129
+ ) -> MeshLlmAgent:
130
+ """Factory to create MeshLlmAgent with context for template rendering."""
131
+ return self._create_llm_agent(
132
+ _func_id, context_value=context_value
133
+ )
134
+
135
+ wrapper._mesh_create_context_agent = create_context_agent
136
+ logger.info(
137
+ f"🎯 Set context agent factory for template-based function '{function_id}' (direct LiteLLM mode)"
138
+ )
139
+
68
140
  def _build_function_name_to_id_mapping(self) -> dict[str, str]:
69
141
  """
70
142
  Build mapping from function_name to function_id.
@@ -176,16 +248,49 @@ class MeshLlmAgentInjector(BaseInjector):
176
248
  f"✅ Set provider proxy for '{function_id}': {provider_proxy.function_name} at {provider_proxy.endpoint} (vendor={vendor})"
177
249
  )
178
250
 
179
- # Re-create and update MeshLlmAgent with new provider (only if tools are also available)
180
- # Get the function wrapper from DecoratorRegistry
251
+ # Re-create and update MeshLlmAgent with new provider
252
+ # Get the function wrapper and metadata from DecoratorRegistry
181
253
  llm_agents = DecoratorRegistry.get_mesh_llm_agents()
182
254
  wrapper = None
255
+ llm_metadata = None
183
256
  for agent_func_id, metadata in llm_agents.items():
184
257
  if metadata.function_id == function_id:
185
258
  wrapper = metadata.function
259
+ llm_metadata = metadata
186
260
  break
187
261
 
188
- # Only update wrapper if we have both tools and provider (tools_metadata indicates tools were processed)
262
+ # Check if tools are required (filter is specified)
263
+ has_filter = False
264
+ if llm_metadata and llm_metadata.config:
265
+ filter_config = llm_metadata.config.get("filter")
266
+ has_filter = filter_config is not None and len(filter_config) > 0
267
+
268
+ # CRITICAL FIX: Always set config if not already set (prevents KeyError during race condition)
269
+ # When provider arrives before tools, config must be available for inject_llm_agent
270
+ # to create the LLM agent with context. Without this, _create_llm_agent fails with KeyError.
271
+ if "config" not in self._llm_agents[function_id] and llm_metadata:
272
+ self._llm_agents[function_id]["config"] = llm_metadata.config
273
+ self._llm_agents[function_id]["output_type"] = llm_metadata.output_type
274
+ self._llm_agents[function_id]["param_name"] = llm_metadata.param_name
275
+ self._llm_agents[function_id]["function"] = llm_metadata.function
276
+ logger.debug(
277
+ f"📋 Set config from DecoratorRegistry for '{function_id}' (awaiting tools)"
278
+ )
279
+
280
+ # If no filter specified, initialize empty tools data so we can create LLM agent without tools
281
+ # This supports simple LLM calls (text generation) that don't need tool calling
282
+ if not has_filter and "tools_metadata" not in self._llm_agents[function_id]:
283
+ self._llm_agents[function_id].update(
284
+ {
285
+ "tools_metadata": [], # No tools for simple LLM calls
286
+ "tools_proxies": {}, # No tool proxies needed
287
+ }
288
+ )
289
+ logger.info(
290
+ f"✅ Initialized empty tools for '{function_id}' (no filter specified - simple LLM mode)"
291
+ )
292
+
293
+ # Update wrapper if we have tools data (either from filter matching or initialized empty)
189
294
  if (
190
295
  wrapper
191
296
  and hasattr(wrapper, "_mesh_update_llm_agent")
@@ -194,9 +299,26 @@ class MeshLlmAgentInjector(BaseInjector):
194
299
  llm_agent = self._create_llm_agent(function_id)
195
300
  wrapper._mesh_update_llm_agent(llm_agent)
196
301
  logger.info(
197
- f"🔄 Updated wrapper with new MeshLlmAgent (with provider) for '{function_id}'"
302
+ f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}'"
303
+ + (" (with tools)" if has_filter else " (simple LLM mode)")
198
304
  )
199
- elif wrapper and hasattr(wrapper, "_mesh_update_llm_agent"):
305
+
306
+ # Set factory for per-call context agent creation (template support)
307
+ # This is critical for filter=None cases where _process_function_tools isn't called
308
+ config_dict = llm_metadata.config if llm_metadata else {}
309
+ if config_dict.get("is_template", False):
310
+ # Capture function_id by value using default argument to avoid closure issues
311
+ def create_context_agent(
312
+ context_value: Any, _func_id: str = function_id
313
+ ) -> MeshLlmAgent:
314
+ """Factory to create MeshLlmAgent with context for template rendering."""
315
+ return self._create_llm_agent(_func_id, context_value=context_value)
316
+
317
+ wrapper._mesh_create_context_agent = create_context_agent
318
+ logger.info(
319
+ f"🎯 Set context agent factory for template-based function '{function_id}' (simple LLM mode)"
320
+ )
321
+ elif wrapper and hasattr(wrapper, "_mesh_update_llm_agent") and has_filter:
200
322
  logger.debug(
201
323
  f"⏳ Provider set for '{function_id}', waiting for tools before updating wrapper"
202
324
  )
@@ -432,36 +554,62 @@ class MeshLlmAgentInjector(BaseInjector):
432
554
  def inject_llm_agent(func: Callable, args: tuple, kwargs: dict) -> tuple:
433
555
  """Inject LLM agent into kwargs if not provided."""
434
556
  if param_name not in kwargs or kwargs.get(param_name) is None:
435
- # Phase 4: Check if templates are enabled
557
+ # Get config from runtime data or fallback to decorator registry.
558
+ # Runtime data (self._llm_agents) is populated during heartbeat and has
559
+ # tools/provider info. Decorator registry is populated at decorator time
560
+ # and always has config/context_param. For self-dependency calls that
561
+ # happen before heartbeat, we need the decorator registry fallback.
562
+ agent_data = None
563
+ config_dict = None
564
+
565
+ # Try runtime data first (has tools, provider from heartbeat)
436
566
  if function_id in self._llm_agents:
437
567
  agent_data = self._llm_agents[function_id]
438
- config_dict = agent_data["config"]
439
- is_template = config_dict.get("is_template", False)
440
-
441
- if is_template:
442
- # Templates enabled - create per-call agent with context
443
- # Import signature analyzer for context detection
444
- from .signature_analyzer import get_context_parameter_name
445
-
446
- # Detect context parameter
447
- context_param_name = config_dict.get("context_param")
448
- context_info = get_context_parameter_name(
449
- func, explicit_name=context_param_name
568
+ config_dict = agent_data.get("config")
569
+
570
+ # Fallback to decorator registry (always available, has context_param)
571
+ # This is critical for self-dependency calls that happen before heartbeat
572
+ if config_dict is None:
573
+ llm_agents_registry = DecoratorRegistry.get_mesh_llm_agents()
574
+ if function_id in llm_agents_registry:
575
+ llm_metadata = llm_agents_registry[function_id]
576
+ config_dict = llm_metadata.config
577
+ logger.debug(
578
+ f"🔄 Using DecoratorRegistry fallback for '{function_id}' config (self-dependency before heartbeat)"
450
579
  )
451
580
 
452
- # Extract context value from call
453
- context_value = None
454
- if context_info is not None:
455
- ctx_name, ctx_index = context_info
581
+ # Check if templates are enabled
582
+ is_template = (
583
+ config_dict.get("is_template", False) if config_dict else False
584
+ )
456
585
 
457
- # Try kwargs first
458
- if ctx_name in kwargs:
459
- context_value = kwargs[ctx_name]
460
- # Then try positional args
461
- elif ctx_index < len(args):
462
- context_value = args[ctx_index]
586
+ if is_template and config_dict:
587
+ # Templates enabled - create per-call agent with context
588
+ # Import signature analyzer for context detection
589
+ from .signature_analyzer import get_context_parameter_name
463
590
 
464
- # Create agent with context for this call
591
+ # Detect context parameter
592
+ context_param_name = config_dict.get("context_param")
593
+ context_info = get_context_parameter_name(
594
+ func, explicit_name=context_param_name
595
+ )
596
+
597
+ # Extract context value from call
598
+ context_value = None
599
+ if context_info is not None:
600
+ ctx_name, ctx_index = context_info
601
+
602
+ # Try kwargs first
603
+ if ctx_name in kwargs:
604
+ context_value = kwargs[ctx_name]
605
+ # Then try positional args
606
+ elif ctx_index < len(args):
607
+ context_value = args[ctx_index]
608
+
609
+ # Create agent with context for this call
610
+ # Note: _create_llm_agent requires function_id in self._llm_agents
611
+ # If not available yet, use cached agent with context_value set directly
612
+ if function_id in self._llm_agents:
465
613
  current_agent = self._create_llm_agent(
466
614
  function_id, context_value=context_value
467
615
  )
@@ -469,22 +617,41 @@ class MeshLlmAgentInjector(BaseInjector):
469
617
  f"🤖 Created MeshLlmAgent with context for {func.__name__}.{param_name}"
470
618
  )
471
619
  else:
472
- # No template - use cached agent (existing behavior)
620
+ # Runtime data not yet available - use cached agent but log warning
621
+ # The cached agent may have been created without context
473
622
  current_agent = wrapper._mesh_llm_agent
474
623
  if current_agent is not None:
475
- logger.debug(
476
- f"🤖 Injected MeshLlmAgent into {func.__name__}.{param_name}"
477
- )
624
+ # Update context on the cached agent if possible
625
+ if hasattr(current_agent, "_context_value"):
626
+ current_agent._context_value = context_value
627
+ logger.debug(
628
+ f"🤖 Updated context on cached MeshLlmAgent for {func.__name__}.{param_name}"
629
+ )
630
+ else:
631
+ logger.debug(
632
+ f"🤖 Injected cached MeshLlmAgent into {func.__name__}.{param_name} (context may not be applied)"
633
+ )
478
634
  else:
479
635
  logger.warning(
480
636
  f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
481
637
  )
638
+ elif config_dict:
639
+ # No template - use cached agent (existing behavior)
640
+ current_agent = wrapper._mesh_llm_agent
641
+ if current_agent is not None:
642
+ logger.debug(
643
+ f"🤖 Injected MeshLlmAgent into {func.__name__}.{param_name}"
644
+ )
645
+ else:
646
+ logger.warning(
647
+ f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
648
+ )
482
649
  else:
483
- # No agent data - use cached (backward compatibility)
650
+ # No config found anywhere - use cached (backward compatibility)
484
651
  current_agent = wrapper._mesh_llm_agent
485
652
  if current_agent is None:
486
653
  logger.warning(
487
- f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
654
+ f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (no config found)"
488
655
  )
489
656
 
490
657
  kwargs[param_name] = current_agent
@@ -585,13 +752,17 @@ class MeshLlmAgentInjector(BaseInjector):
585
752
  )
586
753
 
587
754
  # Create MeshLlmAgent with both metadata and proxies
755
+ # Use .get() with defaults for tools_metadata/proxies to handle race condition
756
+ # where provider arrives before tools (filter-based agents)
588
757
  llm_agent = MeshLlmAgent(
589
758
  config=llm_config,
590
- filtered_tools=llm_agent_data[
591
- "tools_metadata"
592
- ], # Metadata for schema building
759
+ filtered_tools=llm_agent_data.get(
760
+ "tools_metadata", []
761
+ ), # Metadata for schema building (empty if tools not yet received)
593
762
  output_type=llm_agent_data["output_type"],
594
- tool_proxies=llm_agent_data["tools_proxies"], # Proxies for execution
763
+ tool_proxies=llm_agent_data.get(
764
+ "tools_proxies", {}
765
+ ), # Proxies for execution (empty if tools not yet received)
595
766
  template_path=template_path if is_template else None,
596
767
  context_value=context_value if is_template else None,
597
768
  provider_proxy=llm_agent_data.get(
@@ -9,6 +9,7 @@ from .base_provider_handler import (
9
9
  BASE_TOOL_INSTRUCTIONS,
10
10
  CLAUDE_ANTI_XML_INSTRUCTION,
11
11
  BaseProviderHandler,
12
+ is_simple_schema,
12
13
  make_schema_strict,
13
14
  )
14
15
  from .claude_handler import ClaudeHandler
@@ -22,6 +23,7 @@ __all__ = [
22
23
  "BASE_TOOL_INSTRUCTIONS",
23
24
  "CLAUDE_ANTI_XML_INSTRUCTION",
24
25
  # Utilities
26
+ "is_simple_schema",
25
27
  "make_schema_strict",
26
28
  # Handlers
27
29
  "BaseProviderHandler",