gobby 0.2.5__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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/mcp_proxy/lazy.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lazy server initialization with circuit breaker pattern.
|
|
3
|
+
|
|
4
|
+
Provides deferred MCP server connections to reduce startup time and resource usage.
|
|
5
|
+
Servers are connected on-demand when first accessed, with automatic retry and
|
|
6
|
+
circuit breaker protection against cascading failures.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("gobby.mcp.lazy")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CircuitState(str, Enum):
|
|
24
|
+
"""Circuit breaker states."""
|
|
25
|
+
|
|
26
|
+
CLOSED = "closed" # Normal operation, requests pass through
|
|
27
|
+
OPEN = "open" # Circuit tripped, fail fast
|
|
28
|
+
HALF_OPEN = "half_open" # Testing if service recovered
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CircuitBreaker:
|
|
33
|
+
"""
|
|
34
|
+
Circuit breaker for connection protection.
|
|
35
|
+
|
|
36
|
+
Prevents cascading failures by failing fast when a service is down.
|
|
37
|
+
|
|
38
|
+
States:
|
|
39
|
+
- CLOSED: Normal operation, all requests pass through
|
|
40
|
+
- OPEN: Service is down, fail immediately without trying
|
|
41
|
+
- HALF_OPEN: Service may have recovered, allow one test request
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
failure_threshold: int = 3 # Failures before opening circuit
|
|
45
|
+
recovery_timeout: float = 30.0 # Seconds before trying half-open
|
|
46
|
+
half_open_max_calls: int = 1 # Calls allowed in half-open state
|
|
47
|
+
|
|
48
|
+
state: CircuitState = CircuitState.CLOSED
|
|
49
|
+
failure_count: int = 0
|
|
50
|
+
last_failure_time: float | None = None
|
|
51
|
+
half_open_calls: int = 0
|
|
52
|
+
|
|
53
|
+
def record_success(self) -> None:
|
|
54
|
+
"""Record successful operation."""
|
|
55
|
+
self.failure_count = 0
|
|
56
|
+
self.half_open_calls = 0
|
|
57
|
+
self.state = CircuitState.CLOSED
|
|
58
|
+
|
|
59
|
+
def record_failure(self) -> None:
|
|
60
|
+
"""Record failed operation and potentially trip circuit."""
|
|
61
|
+
self.failure_count += 1
|
|
62
|
+
self.last_failure_time = time.time()
|
|
63
|
+
|
|
64
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
65
|
+
# Failed during recovery test, reopen circuit
|
|
66
|
+
self.state = CircuitState.OPEN
|
|
67
|
+
self.half_open_calls = 0
|
|
68
|
+
logger.warning("Circuit breaker reopened after half-open failure")
|
|
69
|
+
elif self.failure_count >= self.failure_threshold:
|
|
70
|
+
self.state = CircuitState.OPEN
|
|
71
|
+
logger.warning(f"Circuit breaker opened after {self.failure_count} failures")
|
|
72
|
+
|
|
73
|
+
def can_execute(self) -> bool:
|
|
74
|
+
"""Check if request can proceed."""
|
|
75
|
+
if self.state == CircuitState.CLOSED:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
if self.state == CircuitState.OPEN:
|
|
79
|
+
# Check if recovery timeout has passed
|
|
80
|
+
if self.last_failure_time is None:
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
elapsed = time.time() - self.last_failure_time
|
|
84
|
+
if elapsed >= self.recovery_timeout:
|
|
85
|
+
self.state = CircuitState.HALF_OPEN
|
|
86
|
+
self.half_open_calls = 0
|
|
87
|
+
logger.info("Circuit breaker entering half-open state")
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
92
|
+
if self.half_open_calls < self.half_open_max_calls:
|
|
93
|
+
self.half_open_calls += 1
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class RetryConfig:
|
|
102
|
+
"""Configuration for exponential backoff retry."""
|
|
103
|
+
|
|
104
|
+
max_retries: int = 3
|
|
105
|
+
initial_delay: float = 1.0 # seconds
|
|
106
|
+
max_delay: float = 16.0 # seconds
|
|
107
|
+
multiplier: float = 2.0
|
|
108
|
+
|
|
109
|
+
def get_delay(self, attempt: int) -> float:
|
|
110
|
+
"""Get delay for given attempt number (0-indexed)."""
|
|
111
|
+
delay = self.initial_delay * (self.multiplier**attempt)
|
|
112
|
+
return min(delay, self.max_delay)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class LazyConnectionState:
|
|
117
|
+
"""
|
|
118
|
+
State tracking for a lazy-connected server.
|
|
119
|
+
|
|
120
|
+
Tracks whether a server has been connected, its circuit breaker state,
|
|
121
|
+
and connection timing information.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
configured_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
125
|
+
connected_at: datetime | None = None
|
|
126
|
+
last_attempt_at: datetime | None = None
|
|
127
|
+
last_error: str | None = None
|
|
128
|
+
connection_attempts: int = 0
|
|
129
|
+
circuit_breaker: CircuitBreaker = field(default_factory=CircuitBreaker)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def is_connected(self) -> bool:
|
|
133
|
+
"""Check if server has been successfully connected."""
|
|
134
|
+
return self.connected_at is not None
|
|
135
|
+
|
|
136
|
+
def record_connection_attempt(self) -> None:
|
|
137
|
+
"""Record that a connection attempt is starting."""
|
|
138
|
+
self.last_attempt_at = datetime.now(UTC)
|
|
139
|
+
self.connection_attempts += 1
|
|
140
|
+
|
|
141
|
+
def record_connection_success(self) -> None:
|
|
142
|
+
"""Record successful connection."""
|
|
143
|
+
self.connected_at = datetime.now(UTC)
|
|
144
|
+
self.last_error = None
|
|
145
|
+
self.circuit_breaker.record_success()
|
|
146
|
+
|
|
147
|
+
def record_connection_failure(self, error: str) -> None:
|
|
148
|
+
"""Record failed connection."""
|
|
149
|
+
self.last_error = error
|
|
150
|
+
self.circuit_breaker.record_failure()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class LazyServerConnector:
|
|
154
|
+
"""
|
|
155
|
+
Manages lazy initialization of MCP server connections.
|
|
156
|
+
|
|
157
|
+
Instead of connecting all servers at startup, connections are deferred
|
|
158
|
+
until the first tool call or explicit request. This reduces startup time
|
|
159
|
+
and avoids consuming resources for unused servers.
|
|
160
|
+
|
|
161
|
+
Features:
|
|
162
|
+
- Deferred connection on first use
|
|
163
|
+
- Exponential backoff retry on connection failure
|
|
164
|
+
- Circuit breaker to prevent cascading failures
|
|
165
|
+
- Connection state tracking and reporting
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
retry_config: RetryConfig | None = None,
|
|
171
|
+
circuit_breaker_config: dict[str, Any] | None = None,
|
|
172
|
+
):
|
|
173
|
+
"""
|
|
174
|
+
Initialize lazy connector.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
retry_config: Retry configuration for connection attempts
|
|
178
|
+
circuit_breaker_config: Circuit breaker settings (failure_threshold,
|
|
179
|
+
recovery_timeout, half_open_max_calls)
|
|
180
|
+
"""
|
|
181
|
+
self.retry_config = retry_config or RetryConfig()
|
|
182
|
+
self._circuit_breaker_config = circuit_breaker_config or {}
|
|
183
|
+
self._states: dict[str, LazyConnectionState] = {}
|
|
184
|
+
self._connection_locks: dict[str, asyncio.Lock] = {}
|
|
185
|
+
|
|
186
|
+
def register_server(self, server_name: str) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Register a server for lazy connection.
|
|
189
|
+
|
|
190
|
+
Called when a server is configured but not yet connected.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
server_name: Name of the server to register
|
|
194
|
+
"""
|
|
195
|
+
if server_name not in self._states:
|
|
196
|
+
self._states[server_name] = LazyConnectionState(
|
|
197
|
+
circuit_breaker=CircuitBreaker(**self._circuit_breaker_config)
|
|
198
|
+
)
|
|
199
|
+
self._connection_locks[server_name] = asyncio.Lock()
|
|
200
|
+
logger.debug(f"Registered server '{server_name}' for lazy connection")
|
|
201
|
+
|
|
202
|
+
def unregister_server(self, server_name: str) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Remove a server from lazy connection tracking.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
server_name: Name of the server to unregister
|
|
208
|
+
"""
|
|
209
|
+
self._states.pop(server_name, None)
|
|
210
|
+
self._connection_locks.pop(server_name, None)
|
|
211
|
+
|
|
212
|
+
def get_state(self, server_name: str) -> LazyConnectionState | None:
|
|
213
|
+
"""
|
|
214
|
+
Get connection state for a server.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
server_name: Name of the server
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
LazyConnectionState or None if not registered
|
|
221
|
+
"""
|
|
222
|
+
return self._states.get(server_name)
|
|
223
|
+
|
|
224
|
+
def is_connected(self, server_name: str) -> bool:
|
|
225
|
+
"""
|
|
226
|
+
Check if a server is connected.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
server_name: Name of the server
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if connected, False otherwise
|
|
233
|
+
"""
|
|
234
|
+
state = self._states.get(server_name)
|
|
235
|
+
return state.is_connected if state else False
|
|
236
|
+
|
|
237
|
+
def can_attempt_connection(self, server_name: str) -> bool:
|
|
238
|
+
"""
|
|
239
|
+
Check if connection attempt is allowed (circuit breaker not open).
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
server_name: Name of the server
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if connection can be attempted
|
|
246
|
+
"""
|
|
247
|
+
state = self._states.get(server_name)
|
|
248
|
+
if not state:
|
|
249
|
+
return True # Unknown server, allow attempt
|
|
250
|
+
return state.circuit_breaker.can_execute()
|
|
251
|
+
|
|
252
|
+
def mark_connected(self, server_name: str) -> None:
|
|
253
|
+
"""
|
|
254
|
+
Mark a server as successfully connected.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
server_name: Name of the server
|
|
258
|
+
"""
|
|
259
|
+
state = self._states.get(server_name)
|
|
260
|
+
if state:
|
|
261
|
+
state.record_connection_success()
|
|
262
|
+
logger.info(f"Server '{server_name}' connected")
|
|
263
|
+
|
|
264
|
+
def mark_failed(self, server_name: str, error: str) -> None:
|
|
265
|
+
"""
|
|
266
|
+
Mark a server connection as failed.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
server_name: Name of the server
|
|
270
|
+
error: Error message
|
|
271
|
+
"""
|
|
272
|
+
state = self._states.get(server_name)
|
|
273
|
+
if state:
|
|
274
|
+
state.record_connection_failure(error)
|
|
275
|
+
logger.warning(f"Server '{server_name}' connection failed: {error}")
|
|
276
|
+
|
|
277
|
+
def get_connection_lock(self, server_name: str) -> asyncio.Lock:
|
|
278
|
+
"""
|
|
279
|
+
Get lock for serializing connection attempts to a server.
|
|
280
|
+
|
|
281
|
+
Prevents multiple concurrent connection attempts to the same server.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
server_name: Name of the server
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
asyncio.Lock for the server
|
|
288
|
+
"""
|
|
289
|
+
if server_name not in self._connection_locks:
|
|
290
|
+
self._connection_locks[server_name] = asyncio.Lock()
|
|
291
|
+
return self._connection_locks[server_name]
|
|
292
|
+
|
|
293
|
+
def get_all_states(self) -> dict[str, dict[str, Any]]:
|
|
294
|
+
"""
|
|
295
|
+
Get connection states for all registered servers.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Dict mapping server names to state information
|
|
299
|
+
"""
|
|
300
|
+
return {
|
|
301
|
+
name: {
|
|
302
|
+
"is_connected": state.is_connected,
|
|
303
|
+
"configured_at": state.configured_at.isoformat(),
|
|
304
|
+
"connected_at": state.connected_at.isoformat() if state.connected_at else None,
|
|
305
|
+
"last_attempt_at": (
|
|
306
|
+
state.last_attempt_at.isoformat() if state.last_attempt_at else None
|
|
307
|
+
),
|
|
308
|
+
"last_error": state.last_error,
|
|
309
|
+
"connection_attempts": state.connection_attempts,
|
|
310
|
+
"circuit_state": state.circuit_breaker.state.value,
|
|
311
|
+
"circuit_failures": state.circuit_breaker.failure_count,
|
|
312
|
+
}
|
|
313
|
+
for name, state in self._states.items()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class CircuitBreakerOpen(Exception):
|
|
318
|
+
"""Raised when circuit breaker prevents connection attempt."""
|
|
319
|
+
|
|
320
|
+
def __init__(self, server_name: str, recovery_in: float):
|
|
321
|
+
self.server_name = server_name
|
|
322
|
+
self.recovery_in = recovery_in
|
|
323
|
+
super().__init__(
|
|
324
|
+
f"Circuit breaker open for '{server_name}'. Recovery attempt in {recovery_in:.1f}s"
|
|
325
|
+
)
|