foundry-mcp 0.8.22__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.

Potentially problematic release.


This version of foundry-mcp might be problematic. Click here for more details.

Files changed (153) hide show
  1. foundry_mcp/__init__.py +13 -0
  2. foundry_mcp/cli/__init__.py +67 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +640 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +667 -0
  15. foundry_mcp/cli/commands/session.py +472 -0
  16. foundry_mcp/cli/commands/specs.py +686 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +298 -0
  22. foundry_mcp/cli/logging.py +212 -0
  23. foundry_mcp/cli/main.py +44 -0
  24. foundry_mcp/cli/output.py +122 -0
  25. foundry_mcp/cli/registry.py +110 -0
  26. foundry_mcp/cli/resilience.py +178 -0
  27. foundry_mcp/cli/transcript.py +217 -0
  28. foundry_mcp/config.py +1454 -0
  29. foundry_mcp/core/__init__.py +144 -0
  30. foundry_mcp/core/ai_consultation.py +1773 -0
  31. foundry_mcp/core/batch_operations.py +1202 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/health.py +749 -0
  40. foundry_mcp/core/intake.py +933 -0
  41. foundry_mcp/core/journal.py +700 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1376 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +146 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +387 -0
  57. foundry_mcp/core/prometheus.py +564 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +691 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
  61. foundry_mcp/core/prompts/plan_review.py +627 -0
  62. foundry_mcp/core/providers/__init__.py +237 -0
  63. foundry_mcp/core/providers/base.py +515 -0
  64. foundry_mcp/core/providers/claude.py +472 -0
  65. foundry_mcp/core/providers/codex.py +637 -0
  66. foundry_mcp/core/providers/cursor_agent.py +630 -0
  67. foundry_mcp/core/providers/detectors.py +515 -0
  68. foundry_mcp/core/providers/gemini.py +426 -0
  69. foundry_mcp/core/providers/opencode.py +718 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +308 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +857 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/research/__init__.py +68 -0
  78. foundry_mcp/core/research/memory.py +528 -0
  79. foundry_mcp/core/research/models.py +1234 -0
  80. foundry_mcp/core/research/providers/__init__.py +40 -0
  81. foundry_mcp/core/research/providers/base.py +242 -0
  82. foundry_mcp/core/research/providers/google.py +507 -0
  83. foundry_mcp/core/research/providers/perplexity.py +442 -0
  84. foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
  85. foundry_mcp/core/research/providers/tavily.py +383 -0
  86. foundry_mcp/core/research/workflows/__init__.py +25 -0
  87. foundry_mcp/core/research/workflows/base.py +298 -0
  88. foundry_mcp/core/research/workflows/chat.py +271 -0
  89. foundry_mcp/core/research/workflows/consensus.py +539 -0
  90. foundry_mcp/core/research/workflows/deep_research.py +4142 -0
  91. foundry_mcp/core/research/workflows/ideate.py +682 -0
  92. foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
  93. foundry_mcp/core/resilience.py +600 -0
  94. foundry_mcp/core/responses.py +1624 -0
  95. foundry_mcp/core/review.py +366 -0
  96. foundry_mcp/core/security.py +438 -0
  97. foundry_mcp/core/spec.py +4119 -0
  98. foundry_mcp/core/task.py +2463 -0
  99. foundry_mcp/core/testing.py +839 -0
  100. foundry_mcp/core/validation.py +2357 -0
  101. foundry_mcp/dashboard/__init__.py +32 -0
  102. foundry_mcp/dashboard/app.py +119 -0
  103. foundry_mcp/dashboard/components/__init__.py +17 -0
  104. foundry_mcp/dashboard/components/cards.py +88 -0
  105. foundry_mcp/dashboard/components/charts.py +177 -0
  106. foundry_mcp/dashboard/components/filters.py +136 -0
  107. foundry_mcp/dashboard/components/tables.py +195 -0
  108. foundry_mcp/dashboard/data/__init__.py +11 -0
  109. foundry_mcp/dashboard/data/stores.py +433 -0
  110. foundry_mcp/dashboard/launcher.py +300 -0
  111. foundry_mcp/dashboard/views/__init__.py +12 -0
  112. foundry_mcp/dashboard/views/errors.py +217 -0
  113. foundry_mcp/dashboard/views/metrics.py +164 -0
  114. foundry_mcp/dashboard/views/overview.py +96 -0
  115. foundry_mcp/dashboard/views/providers.py +83 -0
  116. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  117. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  118. foundry_mcp/prompts/__init__.py +9 -0
  119. foundry_mcp/prompts/workflows.py +525 -0
  120. foundry_mcp/resources/__init__.py +9 -0
  121. foundry_mcp/resources/specs.py +591 -0
  122. foundry_mcp/schemas/__init__.py +38 -0
  123. foundry_mcp/schemas/intake-schema.json +89 -0
  124. foundry_mcp/schemas/sdd-spec-schema.json +414 -0
  125. foundry_mcp/server.py +150 -0
  126. foundry_mcp/tools/__init__.py +10 -0
  127. foundry_mcp/tools/unified/__init__.py +92 -0
  128. foundry_mcp/tools/unified/authoring.py +3620 -0
  129. foundry_mcp/tools/unified/context_helpers.py +98 -0
  130. foundry_mcp/tools/unified/documentation_helpers.py +268 -0
  131. foundry_mcp/tools/unified/environment.py +1341 -0
  132. foundry_mcp/tools/unified/error.py +479 -0
  133. foundry_mcp/tools/unified/health.py +225 -0
  134. foundry_mcp/tools/unified/journal.py +841 -0
  135. foundry_mcp/tools/unified/lifecycle.py +640 -0
  136. foundry_mcp/tools/unified/metrics.py +777 -0
  137. foundry_mcp/tools/unified/plan.py +876 -0
  138. foundry_mcp/tools/unified/pr.py +294 -0
  139. foundry_mcp/tools/unified/provider.py +589 -0
  140. foundry_mcp/tools/unified/research.py +1283 -0
  141. foundry_mcp/tools/unified/review.py +1042 -0
  142. foundry_mcp/tools/unified/review_helpers.py +314 -0
  143. foundry_mcp/tools/unified/router.py +102 -0
  144. foundry_mcp/tools/unified/server.py +565 -0
  145. foundry_mcp/tools/unified/spec.py +1283 -0
  146. foundry_mcp/tools/unified/task.py +3846 -0
  147. foundry_mcp/tools/unified/test.py +431 -0
  148. foundry_mcp/tools/unified/verification.py +520 -0
  149. foundry_mcp-0.8.22.dist-info/METADATA +344 -0
  150. foundry_mcp-0.8.22.dist-info/RECORD +153 -0
  151. foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
  152. foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
  153. foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,607 @@
1
+ """
2
+ Provider registry utilities.
3
+
4
+ Encapsulates registration, lazy loading, availability checks, and dependency
5
+ injection hooks for ProviderContext implementations. This module backs the
6
+ CLI provider runner plus future skill integrations, providing a single source
7
+ of truth for discovering available providers.
8
+
9
+ Example:
10
+ >>> from foundry_mcp.core.providers.registry import (
11
+ ... register_provider,
12
+ ... resolve_provider,
13
+ ... available_providers,
14
+ ... )
15
+ >>> from foundry_mcp.core.providers import ProviderHooks
16
+ >>>
17
+ >>> # Register a provider factory
18
+ >>> register_provider(
19
+ ... "my-provider",
20
+ ... factory=my_provider_factory,
21
+ ... description="My custom provider",
22
+ ... )
23
+ >>>
24
+ >>> # List available providers
25
+ >>> available_providers()
26
+ ['my-provider']
27
+ >>>
28
+ >>> # Resolve and instantiate a provider
29
+ >>> hooks = ProviderHooks()
30
+ >>> provider = resolve_provider("my-provider", hooks=hooks)
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import importlib
36
+ import logging
37
+ from dataclasses import dataclass, field
38
+ from typing import Any, Callable, Dict, List, Optional, Protocol, Sequence
39
+
40
+ from foundry_mcp.core.providers.base import (
41
+ ProviderContext,
42
+ ProviderHooks,
43
+ ProviderMetadata,
44
+ ProviderUnavailableError,
45
+ )
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ # =============================================================================
51
+ # Type Definitions
52
+ # =============================================================================
53
+
54
+
55
+ class ProviderFactory(Protocol):
56
+ """
57
+ Callable that instantiates a ProviderContext.
58
+
59
+ Implementations should accept keyword-only arguments for hooks, model,
60
+ dependencies, and overrides so the registry can pass future options
61
+ without breaking signatures.
62
+
63
+ Example:
64
+ def create_my_provider(
65
+ *,
66
+ hooks: ProviderHooks,
67
+ model: Optional[str] = None,
68
+ dependencies: Optional[Dict[str, object]] = None,
69
+ overrides: Optional[Dict[str, object]] = None,
70
+ ) -> ProviderContext:
71
+ return MyProvider(hooks=hooks, model=model)
72
+ """
73
+
74
+ def __call__(
75
+ self,
76
+ *,
77
+ hooks: ProviderHooks,
78
+ model: Optional[str] = None,
79
+ dependencies: Optional[Dict[str, object]] = None,
80
+ overrides: Optional[Dict[str, object]] = None,
81
+ ) -> ProviderContext:
82
+ ...
83
+
84
+
85
+ # Type aliases for registry callables
86
+ AvailabilityCheck = Callable[[], bool]
87
+ MetadataResolver = Callable[[], ProviderMetadata]
88
+ LazyFactoryLoader = Callable[[], ProviderFactory]
89
+ DependencyResolver = Callable[[str], Dict[str, object]]
90
+
91
+
92
+ # =============================================================================
93
+ # Provider Registration
94
+ # =============================================================================
95
+
96
+
97
+ @dataclass
98
+ class ProviderRegistration:
99
+ """
100
+ Internal record for a registered provider.
101
+
102
+ Supports both eager and lazy factory loading, with optional metadata
103
+ resolvers and availability checks.
104
+
105
+ Attributes:
106
+ provider_id: Canonical provider identifier (e.g., "gemini", "codex")
107
+ factory: Callable that instantiates ProviderContext (eager)
108
+ lazy_loader: Callable that returns a factory when invoked (lazy import)
109
+ metadata: Cached ProviderMetadata object
110
+ metadata_resolver: Callable that returns ProviderMetadata on demand
111
+ availability_check: Callable returning bool to gate resolution
112
+ priority: Sorting priority for available_providers (higher first)
113
+ description: Human-readable description for diagnostics
114
+ tags: Optional labels describing the provider (e.g., ["cli", "external"])
115
+ """
116
+
117
+ provider_id: str
118
+ factory: Optional[ProviderFactory] = None
119
+ lazy_loader: Optional[LazyFactoryLoader] = None
120
+ metadata: Optional[ProviderMetadata] = None
121
+ metadata_resolver: Optional[MetadataResolver] = None
122
+ availability_check: Optional[AvailabilityCheck] = None
123
+ priority: int = 0
124
+ description: Optional[str] = None
125
+ tags: Sequence[str] = field(default_factory=tuple)
126
+
127
+ def load_factory(self) -> ProviderFactory:
128
+ """
129
+ Return the provider factory, performing lazy import if needed.
130
+
131
+ Returns:
132
+ The provider factory callable
133
+
134
+ Raises:
135
+ ProviderUnavailableError: If no factory is available
136
+ """
137
+ if self.factory is not None:
138
+ return self.factory
139
+
140
+ if self.lazy_loader is None:
141
+ raise ProviderUnavailableError(
142
+ f"Provider '{self.provider_id}' is missing a factory.",
143
+ provider=self.provider_id,
144
+ )
145
+
146
+ factory = self.lazy_loader()
147
+ if factory is None:
148
+ raise ProviderUnavailableError(
149
+ f"Lazy loader for '{self.provider_id}' returned None.",
150
+ provider=self.provider_id,
151
+ )
152
+
153
+ # Cache the factory for future calls
154
+ self.factory = factory
155
+ self.lazy_loader = None
156
+ return factory
157
+
158
+ def is_available(self) -> bool:
159
+ """
160
+ Return True if the provider passes its availability check.
161
+
162
+ If no availability_check is registered, returns True.
163
+ """
164
+ if self.availability_check is None:
165
+ return True
166
+ try:
167
+ return bool(self.availability_check())
168
+ except Exception as exc: # pragma: no cover - defensive logging
169
+ logger.warning(
170
+ "Availability check for provider '%s' failed: %s",
171
+ self.provider_id,
172
+ exc,
173
+ )
174
+ return False
175
+
176
+ def resolve_metadata(self) -> Optional[ProviderMetadata]:
177
+ """
178
+ Return cached metadata, resolving lazily when necessary.
179
+
180
+ Returns:
181
+ ProviderMetadata if available, None otherwise
182
+ """
183
+ if self.metadata is not None:
184
+ return self.metadata
185
+
186
+ if self.metadata_resolver is None:
187
+ return None
188
+
189
+ try:
190
+ self.metadata = self.metadata_resolver()
191
+ return self.metadata
192
+ except Exception as exc: # pragma: no cover - defensive logging
193
+ logger.warning(
194
+ "Metadata resolver for provider '%s' failed: %s",
195
+ self.provider_id,
196
+ exc,
197
+ )
198
+ return None
199
+
200
+
201
+ # Global registry state
202
+ _REGISTRY: Dict[str, ProviderRegistration] = {}
203
+ _dependency_resolver: Optional[DependencyResolver] = None
204
+
205
+
206
+ # =============================================================================
207
+ # Public API - Registration
208
+ # =============================================================================
209
+
210
+
211
+ def register_provider(
212
+ provider_id: str,
213
+ *,
214
+ factory: Optional[ProviderFactory] = None,
215
+ lazy_loader: Optional[LazyFactoryLoader] = None,
216
+ metadata: Optional[ProviderMetadata] = None,
217
+ metadata_resolver: Optional[MetadataResolver] = None,
218
+ availability_check: Optional[AvailabilityCheck] = None,
219
+ priority: int = 0,
220
+ description: Optional[str] = None,
221
+ tags: Optional[Sequence[str]] = None,
222
+ replace: bool = False,
223
+ ) -> None:
224
+ """
225
+ Register a provider factory with the global registry.
226
+
227
+ Args:
228
+ provider_id: Canonical provider identifier (e.g., "gemini")
229
+ factory: Callable that instantiates ProviderContext instances
230
+ lazy_loader: Callable that returns a factory when invoked (lazy import)
231
+ metadata: Optional ProviderMetadata object (cached)
232
+ metadata_resolver: Callable that returns ProviderMetadata on demand
233
+ availability_check: Callable returning bool to gate resolution
234
+ priority: Sorting priority for available_providers (higher first)
235
+ description: Human-readable description for diagnostics
236
+ tags: Optional labels describing the provider
237
+ replace: Overwrite existing registration if True
238
+
239
+ Raises:
240
+ ValueError: If provider_id already registered and replace=False
241
+ ValueError: If neither factory nor lazy_loader provided
242
+
243
+ Example:
244
+ >>> register_provider(
245
+ ... "my-provider",
246
+ ... factory=my_factory,
247
+ ... availability_check=lambda: True,
248
+ ... priority=10,
249
+ ... description="My custom provider",
250
+ ... tags=["external", "experimental"],
251
+ ... )
252
+ """
253
+ if provider_id in _REGISTRY and not replace:
254
+ raise ValueError(f"Provider '{provider_id}' is already registered")
255
+
256
+ if factory is None and lazy_loader is None:
257
+ raise ValueError("Either 'factory' or 'lazy_loader' must be provided")
258
+
259
+ registration = ProviderRegistration(
260
+ provider_id=provider_id,
261
+ factory=factory,
262
+ lazy_loader=lazy_loader,
263
+ metadata=metadata,
264
+ metadata_resolver=metadata_resolver,
265
+ availability_check=availability_check,
266
+ priority=priority,
267
+ description=description,
268
+ tags=tuple(tags or ()),
269
+ )
270
+ _REGISTRY[provider_id] = registration
271
+ logger.debug("Provider '%s' registered (priority=%s)", provider_id, priority)
272
+
273
+
274
+ def register_lazy_provider(
275
+ provider_id: str,
276
+ module_path: str,
277
+ *,
278
+ factory_attr: str = "create_provider",
279
+ metadata_attr: Optional[str] = None,
280
+ availability_attr: Optional[str] = None,
281
+ priority: int = 0,
282
+ description: Optional[str] = None,
283
+ tags: Optional[Sequence[str]] = None,
284
+ replace: bool = False,
285
+ ) -> None:
286
+ """
287
+ Register a provider by module path without importing upfront.
288
+
289
+ This is useful for providers with heavy dependencies that should only
290
+ be loaded when actually needed.
291
+
292
+ Args:
293
+ provider_id: Canonical provider identifier
294
+ module_path: Full module path (e.g., "mypackage.providers.gemini")
295
+ factory_attr: Attribute name for factory function (default: "create_provider")
296
+ metadata_attr: Attribute name for metadata (callable or static)
297
+ availability_attr: Attribute name for availability check (callable)
298
+ priority: Sorting priority for available_providers (higher first)
299
+ description: Human-readable description
300
+ tags: Optional labels
301
+ replace: Overwrite existing registration if True
302
+
303
+ Example:
304
+ >>> register_lazy_provider(
305
+ ... "gemini",
306
+ ... "foundry_mcp.providers.gemini",
307
+ ... factory_attr="create_gemini_provider",
308
+ ... availability_attr="is_gemini_available",
309
+ ... )
310
+ """
311
+
312
+ def _lazy_loader() -> ProviderFactory:
313
+ module = importlib.import_module(module_path)
314
+ factory_obj = getattr(module, factory_attr, None)
315
+ if factory_obj is None:
316
+ raise ProviderUnavailableError(
317
+ f"Module '{module_path}' is missing '{factory_attr}'.",
318
+ provider=provider_id,
319
+ )
320
+ return factory_obj
321
+
322
+ metadata_resolver: Optional[MetadataResolver] = None
323
+ if metadata_attr:
324
+ metadata_resolver = _build_attr_resolver(
325
+ module_path, metadata_attr, provider_id
326
+ )
327
+
328
+ availability_check: Optional[AvailabilityCheck] = None
329
+ if availability_attr:
330
+ availability_check = _build_attr_resolver(
331
+ module_path, availability_attr, provider_id
332
+ )
333
+
334
+ register_provider(
335
+ provider_id,
336
+ lazy_loader=_lazy_loader,
337
+ metadata_resolver=metadata_resolver,
338
+ availability_check=availability_check,
339
+ priority=priority,
340
+ description=description,
341
+ tags=tags,
342
+ replace=replace,
343
+ )
344
+
345
+
346
+ def _build_attr_resolver(
347
+ module_path: str, attr: str, provider_id: str
348
+ ) -> Callable[[], Any]:
349
+ """Build a lazy attribute resolver for a module."""
350
+
351
+ def _resolver() -> Any:
352
+ module = importlib.import_module(module_path)
353
+ target = getattr(module, attr, None)
354
+ if callable(target):
355
+ return target()
356
+ return target
357
+
358
+ return _resolver
359
+
360
+
361
+ # =============================================================================
362
+ # Public API - Resolution
363
+ # =============================================================================
364
+
365
+
366
+ def available_providers(*, include_unavailable: bool = False) -> List[str]:
367
+ """
368
+ Return provider identifiers sorted by priority (desc) then name.
369
+
370
+ Args:
371
+ include_unavailable: If True, include providers that fail availability check
372
+
373
+ Returns:
374
+ List of provider IDs sorted by priority (descending), then alphabetically
375
+
376
+ Example:
377
+ >>> available_providers()
378
+ ['gemini', 'codex', 'cursor-agent']
379
+ >>> available_providers(include_unavailable=True)
380
+ ['gemini', 'codex', 'cursor-agent', 'opencode']
381
+ """
382
+ providers: List[ProviderRegistration] = list(_REGISTRY.values())
383
+ providers.sort(key=lambda reg: (-reg.priority, reg.provider_id))
384
+
385
+ if include_unavailable:
386
+ return [reg.provider_id for reg in providers]
387
+
388
+ return [reg.provider_id for reg in providers if reg.is_available()]
389
+
390
+
391
+ def check_provider_available(provider_id: str) -> bool:
392
+ """
393
+ Check if a provider is available using its registered availability check.
394
+
395
+ Args:
396
+ provider_id: Provider identifier (e.g., "gemini", "codex")
397
+
398
+ Returns:
399
+ True if provider is registered and passes its availability check,
400
+ False otherwise
401
+
402
+ Example:
403
+ >>> check_provider_available("gemini")
404
+ True
405
+ >>> check_provider_available("nonexistent")
406
+ False
407
+ """
408
+ registration = _REGISTRY.get(provider_id)
409
+ if registration is None:
410
+ return False
411
+ return registration.is_available()
412
+
413
+
414
+ def resolve_provider(
415
+ provider_id: str,
416
+ *,
417
+ hooks: ProviderHooks,
418
+ model: Optional[str] = None,
419
+ overrides: Optional[Dict[str, object]] = None,
420
+ ) -> ProviderContext:
421
+ """
422
+ Instantiate a provider by ID using the registered factory.
423
+
424
+ Args:
425
+ provider_id: Provider identifier (e.g., "gemini", "codex")
426
+ hooks: Lifecycle hooks to wire into the provider
427
+ model: Optional model override
428
+ overrides: Optional provider-specific configuration overrides
429
+
430
+ Returns:
431
+ Instantiated ProviderContext
432
+
433
+ Raises:
434
+ ProviderUnavailableError: If provider not registered or unavailable
435
+
436
+ Example:
437
+ >>> hooks = ProviderHooks()
438
+ >>> provider = resolve_provider("gemini", hooks=hooks, model="pro")
439
+ >>> result = provider.generate(request)
440
+ """
441
+ registration = _REGISTRY.get(provider_id)
442
+ if registration is None:
443
+ raise ProviderUnavailableError(
444
+ f"Provider '{provider_id}' is not registered.", provider=provider_id
445
+ )
446
+
447
+ if not registration.is_available():
448
+ raise ProviderUnavailableError(
449
+ f"Provider '{provider_id}' is currently unavailable.",
450
+ provider=provider_id,
451
+ )
452
+
453
+ factory = registration.load_factory()
454
+ dependencies = _resolve_dependencies(provider_id)
455
+ return factory(
456
+ hooks=hooks,
457
+ model=model,
458
+ dependencies=dependencies,
459
+ overrides=overrides,
460
+ )
461
+
462
+
463
+ def get_provider_metadata(provider_id: str) -> Optional[ProviderMetadata]:
464
+ """
465
+ Return ProviderMetadata for a registered provider.
466
+
467
+ Args:
468
+ provider_id: Provider identifier
469
+
470
+ Returns:
471
+ ProviderMetadata if available, None otherwise
472
+ """
473
+ registration = _REGISTRY.get(provider_id)
474
+ if registration is None:
475
+ return None
476
+ return registration.resolve_metadata()
477
+
478
+
479
+ def describe_providers() -> List[Dict[str, object]]:
480
+ """
481
+ Return descriptive information for all registered providers.
482
+
483
+ Returns:
484
+ List of dicts with provider info (id, description, priority, tags, available)
485
+
486
+ Example:
487
+ >>> describe_providers()
488
+ [
489
+ {
490
+ "id": "gemini",
491
+ "description": "Google Gemini CLI",
492
+ "priority": 10,
493
+ "tags": ["external", "cli"],
494
+ "available": True,
495
+ },
496
+ ...
497
+ ]
498
+ """
499
+ summary: List[Dict[str, object]] = []
500
+ for reg in _REGISTRY.values():
501
+ summary.append(
502
+ {
503
+ "id": reg.provider_id,
504
+ "description": reg.description,
505
+ "priority": reg.priority,
506
+ "tags": list(reg.tags),
507
+ "available": reg.is_available(),
508
+ }
509
+ )
510
+ return summary
511
+
512
+
513
+ # =============================================================================
514
+ # Public API - Dependency Injection
515
+ # =============================================================================
516
+
517
+
518
+ def set_dependency_resolver(resolver: Optional[DependencyResolver]) -> None:
519
+ """
520
+ Register a callable that supplies dependency dictionaries per provider ID.
521
+
522
+ The resolver is invoked during `resolve_provider` and should return a dict
523
+ that will be passed to the provider factory via the `dependencies` keyword.
524
+
525
+ Args:
526
+ resolver: Callable taking provider_id, returning dependency dict.
527
+ Pass None to clear the resolver.
528
+
529
+ Example:
530
+ >>> def my_resolver(provider_id: str) -> Dict[str, object]:
531
+ ... if provider_id == "gemini":
532
+ ... return {"api_client": my_api_client}
533
+ ... return {}
534
+ >>> set_dependency_resolver(my_resolver)
535
+ """
536
+ global _dependency_resolver
537
+ _dependency_resolver = resolver
538
+
539
+
540
+ def _resolve_dependencies(provider_id: str) -> Dict[str, object]:
541
+ """Resolve dependencies for a provider using the configured resolver."""
542
+ if _dependency_resolver is None:
543
+ return {}
544
+ try:
545
+ dependencies = _dependency_resolver(provider_id)
546
+ return dependencies or {}
547
+ except Exception as exc: # pragma: no cover - defensive logging
548
+ logger.warning(
549
+ "Dependency resolver failed for provider '%s': %s",
550
+ provider_id,
551
+ exc,
552
+ )
553
+ return {}
554
+
555
+
556
+ # =============================================================================
557
+ # Public API - Testing Support
558
+ # =============================================================================
559
+
560
+
561
+ def reset_registry() -> None:
562
+ """
563
+ Clear the registry and dependency resolver.
564
+
565
+ Primarily used by tests to restore a clean state.
566
+ """
567
+ _REGISTRY.clear()
568
+ set_dependency_resolver(None)
569
+ logger.debug("Registry cleared")
570
+
571
+
572
+ def get_registration(provider_id: str) -> Optional[ProviderRegistration]:
573
+ """
574
+ Get the registration record for a provider (for testing/introspection).
575
+
576
+ Args:
577
+ provider_id: Provider identifier
578
+
579
+ Returns:
580
+ ProviderRegistration if found, None otherwise
581
+ """
582
+ return _REGISTRY.get(provider_id)
583
+
584
+
585
+ __all__ = [
586
+ # Types
587
+ "ProviderFactory",
588
+ "ProviderRegistration",
589
+ "AvailabilityCheck",
590
+ "MetadataResolver",
591
+ "LazyFactoryLoader",
592
+ "DependencyResolver",
593
+ # Registration
594
+ "register_provider",
595
+ "register_lazy_provider",
596
+ # Resolution
597
+ "available_providers",
598
+ "check_provider_available",
599
+ "resolve_provider",
600
+ "get_provider_metadata",
601
+ "describe_providers",
602
+ # Dependency Injection
603
+ "set_dependency_resolver",
604
+ # Testing
605
+ "reset_registry",
606
+ "get_registration",
607
+ ]