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/llm/resolver.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider resolution for AgentExecutors.
|
|
3
|
+
|
|
4
|
+
Implements provider resolution hierarchy:
|
|
5
|
+
1. Explicit provider parameter (highest priority)
|
|
6
|
+
2. Workflow settings
|
|
7
|
+
3. Config.yaml llm_providers section
|
|
8
|
+
4. Hardcoded default ("claude")
|
|
9
|
+
|
|
10
|
+
Provides validation, executor factory, and error handling.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import TYPE_CHECKING, Literal
|
|
17
|
+
|
|
18
|
+
from gobby.llm.executor import AgentExecutor
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from gobby.config.app import DaemonConfig, LLMProvidersConfig
|
|
22
|
+
from gobby.workflows.definitions import WorkflowDefinition
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Valid providers
|
|
27
|
+
SUPPORTED_PROVIDERS = frozenset(["claude", "gemini", "litellm", "codex"])
|
|
28
|
+
|
|
29
|
+
# Default provider when nothing is specified
|
|
30
|
+
DEFAULT_PROVIDER = "claude"
|
|
31
|
+
|
|
32
|
+
# Provider name validation pattern: alphanumeric, hyphens, underscores
|
|
33
|
+
PROVIDER_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
34
|
+
MAX_PROVIDER_NAME_LENGTH = 64
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Exception types
|
|
38
|
+
class ProviderError(Exception):
|
|
39
|
+
"""Base exception for provider errors."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InvalidProviderError(ProviderError):
|
|
45
|
+
"""Raised when a provider name is invalid."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, provider: str | None, reason: str):
|
|
48
|
+
self.provider = provider
|
|
49
|
+
self.reason = reason
|
|
50
|
+
super().__init__(f"Invalid provider '{provider}': {reason}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MissingProviderError(ProviderError):
|
|
54
|
+
"""Raised when no provider can be resolved."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, checked_levels: list[str]):
|
|
57
|
+
self.checked_levels = checked_levels
|
|
58
|
+
levels_str = ", ".join(checked_levels)
|
|
59
|
+
super().__init__(f"No provider found. Checked: {levels_str}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ProviderNotConfiguredError(ProviderError):
|
|
63
|
+
"""Raised when a provider is not configured in llm_providers."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, provider: str, available: list[str]):
|
|
66
|
+
self.provider = provider
|
|
67
|
+
self.available = available
|
|
68
|
+
super().__init__(
|
|
69
|
+
f"Provider '{provider}' is not configured. Available providers: {available}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ExecutorCreationError(ProviderError):
|
|
74
|
+
"""Raised when executor creation fails."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, provider: str, reason: str):
|
|
77
|
+
self.provider = provider
|
|
78
|
+
self.reason = reason
|
|
79
|
+
super().__init__(f"Failed to create executor for '{provider}': {reason}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Resolution source for debugging
|
|
83
|
+
ResolutionSource = Literal["explicit", "workflow", "config", "default"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class ResolvedProvider:
|
|
88
|
+
"""Result of provider resolution."""
|
|
89
|
+
|
|
90
|
+
provider: str
|
|
91
|
+
"""Resolved provider name."""
|
|
92
|
+
|
|
93
|
+
source: ResolutionSource
|
|
94
|
+
"""Where the provider was resolved from."""
|
|
95
|
+
|
|
96
|
+
model: str | None = None
|
|
97
|
+
"""Optional model override from resolution source."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validate_provider_name(provider: str | None) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Validate a provider name.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
provider: Provider name to validate.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Validated provider name.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
InvalidProviderError: If the provider name is invalid.
|
|
112
|
+
"""
|
|
113
|
+
if provider is None:
|
|
114
|
+
raise InvalidProviderError(provider, "Provider name cannot be None")
|
|
115
|
+
|
|
116
|
+
if not provider or not provider.strip():
|
|
117
|
+
raise InvalidProviderError(provider, "Provider name cannot be empty")
|
|
118
|
+
|
|
119
|
+
# Strip whitespace
|
|
120
|
+
provider = provider.strip()
|
|
121
|
+
|
|
122
|
+
# Check for whitespace-only after strip
|
|
123
|
+
if not provider:
|
|
124
|
+
raise InvalidProviderError(provider, "Provider name cannot be whitespace-only")
|
|
125
|
+
|
|
126
|
+
# Check length
|
|
127
|
+
if len(provider) > MAX_PROVIDER_NAME_LENGTH:
|
|
128
|
+
raise InvalidProviderError(
|
|
129
|
+
provider,
|
|
130
|
+
f"Provider name exceeds {MAX_PROVIDER_NAME_LENGTH} characters",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Check pattern
|
|
134
|
+
if not PROVIDER_NAME_PATTERN.match(provider):
|
|
135
|
+
raise InvalidProviderError(
|
|
136
|
+
provider,
|
|
137
|
+
"Provider name contains invalid characters. "
|
|
138
|
+
"Only alphanumeric, hyphens, and underscores allowed.",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return provider
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def resolve_provider(
|
|
145
|
+
explicit_provider: str | None = None,
|
|
146
|
+
workflow: "WorkflowDefinition | None" = None,
|
|
147
|
+
config: "DaemonConfig | None" = None,
|
|
148
|
+
allow_unconfigured: bool = False,
|
|
149
|
+
) -> ResolvedProvider:
|
|
150
|
+
"""
|
|
151
|
+
Resolve which provider to use following the resolution hierarchy.
|
|
152
|
+
|
|
153
|
+
Resolution order (highest to lowest priority):
|
|
154
|
+
1. Explicit provider parameter
|
|
155
|
+
2. Workflow settings (workflow.variables.get("provider"))
|
|
156
|
+
3. Config.yaml llm_providers (first enabled provider)
|
|
157
|
+
4. Hardcoded default ("claude")
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
explicit_provider: Explicitly specified provider (highest priority).
|
|
161
|
+
workflow: Optional workflow definition with provider settings.
|
|
162
|
+
config: Optional daemon config with llm_providers.
|
|
163
|
+
allow_unconfigured: If True, skip config validation for the provider.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
ResolvedProvider with provider name and resolution source.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
InvalidProviderError: If a provider name is invalid.
|
|
170
|
+
MissingProviderError: If no provider can be resolved (shouldn't happen with default).
|
|
171
|
+
ProviderNotConfiguredError: If provider is not in llm_providers (unless allow_unconfigured).
|
|
172
|
+
"""
|
|
173
|
+
checked_levels: list[str] = []
|
|
174
|
+
|
|
175
|
+
# 1. Check explicit provider
|
|
176
|
+
if explicit_provider:
|
|
177
|
+
checked_levels.append("explicit")
|
|
178
|
+
provider = validate_provider_name(explicit_provider)
|
|
179
|
+
logger.debug(f"Resolved provider '{provider}' from explicit parameter")
|
|
180
|
+
|
|
181
|
+
# Validate against config if available and not allowing unconfigured
|
|
182
|
+
if config and config.llm_providers and not allow_unconfigured:
|
|
183
|
+
_validate_provider_configured(provider, config.llm_providers)
|
|
184
|
+
|
|
185
|
+
return ResolvedProvider(provider=provider, source="explicit")
|
|
186
|
+
|
|
187
|
+
# 2. Check workflow settings
|
|
188
|
+
if workflow:
|
|
189
|
+
checked_levels.append("workflow")
|
|
190
|
+
workflow_provider = workflow.variables.get("provider")
|
|
191
|
+
workflow_model = workflow.variables.get("model")
|
|
192
|
+
|
|
193
|
+
if workflow_provider:
|
|
194
|
+
provider = validate_provider_name(workflow_provider)
|
|
195
|
+
logger.debug(f"Resolved provider '{provider}' from workflow variables")
|
|
196
|
+
|
|
197
|
+
if config and config.llm_providers and not allow_unconfigured:
|
|
198
|
+
_validate_provider_configured(provider, config.llm_providers)
|
|
199
|
+
|
|
200
|
+
return ResolvedProvider(
|
|
201
|
+
provider=provider,
|
|
202
|
+
source="workflow",
|
|
203
|
+
model=workflow_model if isinstance(workflow_model, str) else None,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# 3. Check config.yaml llm_providers
|
|
207
|
+
if config and config.llm_providers:
|
|
208
|
+
checked_levels.append("config")
|
|
209
|
+
enabled = config.llm_providers.get_enabled_providers()
|
|
210
|
+
|
|
211
|
+
if enabled:
|
|
212
|
+
# Prefer claude if available
|
|
213
|
+
if "claude" in enabled:
|
|
214
|
+
provider = "claude"
|
|
215
|
+
else:
|
|
216
|
+
provider = enabled[0]
|
|
217
|
+
|
|
218
|
+
logger.debug(f"Resolved provider '{provider}' from config (enabled: {enabled})")
|
|
219
|
+
return ResolvedProvider(provider=provider, source="config")
|
|
220
|
+
|
|
221
|
+
# 4. Hardcoded default
|
|
222
|
+
checked_levels.append("default")
|
|
223
|
+
logger.debug(f"Resolved provider '{DEFAULT_PROVIDER}' from hardcoded default")
|
|
224
|
+
|
|
225
|
+
# Validate default is configured if config exists
|
|
226
|
+
if config and config.llm_providers and not allow_unconfigured:
|
|
227
|
+
enabled = config.llm_providers.get_enabled_providers()
|
|
228
|
+
if enabled and DEFAULT_PROVIDER not in enabled:
|
|
229
|
+
# Default not configured, but we have other providers - use first
|
|
230
|
+
provider = enabled[0]
|
|
231
|
+
logger.debug(f"Default not configured, using first enabled: {provider}")
|
|
232
|
+
return ResolvedProvider(provider=provider, source="config")
|
|
233
|
+
|
|
234
|
+
return ResolvedProvider(provider=DEFAULT_PROVIDER, source="default")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _validate_provider_configured(provider: str, llm_providers: "LLMProvidersConfig") -> None:
|
|
238
|
+
"""
|
|
239
|
+
Validate that a provider is configured in llm_providers.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
provider: Provider name to validate.
|
|
243
|
+
llm_providers: LLM providers configuration.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
ProviderNotConfiguredError: If provider is not configured.
|
|
247
|
+
"""
|
|
248
|
+
enabled = llm_providers.get_enabled_providers()
|
|
249
|
+
|
|
250
|
+
if provider not in enabled:
|
|
251
|
+
raise ProviderNotConfiguredError(provider, enabled)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def create_executor(
|
|
255
|
+
provider: str,
|
|
256
|
+
config: "DaemonConfig | None" = None,
|
|
257
|
+
model: str | None = None,
|
|
258
|
+
) -> AgentExecutor:
|
|
259
|
+
"""
|
|
260
|
+
Create an AgentExecutor for the given provider.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
provider: Provider name (claude, gemini, litellm).
|
|
264
|
+
config: Optional daemon config for provider settings.
|
|
265
|
+
model: Optional model override.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
AgentExecutor instance for the provider.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
InvalidProviderError: If provider name is invalid.
|
|
272
|
+
ExecutorCreationError: If executor creation fails.
|
|
273
|
+
"""
|
|
274
|
+
# Validate provider name
|
|
275
|
+
provider = validate_provider_name(provider)
|
|
276
|
+
|
|
277
|
+
# Get provider-specific config if available
|
|
278
|
+
provider_config = None
|
|
279
|
+
if config and config.llm_providers:
|
|
280
|
+
provider_config = getattr(config.llm_providers, provider, None)
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
if provider == "claude":
|
|
284
|
+
return _create_claude_executor(provider_config, model)
|
|
285
|
+
elif provider == "gemini":
|
|
286
|
+
return _create_gemini_executor(provider_config, model)
|
|
287
|
+
elif provider == "litellm":
|
|
288
|
+
return _create_litellm_executor(provider_config, config, model)
|
|
289
|
+
elif provider == "codex":
|
|
290
|
+
return _create_codex_executor(provider_config, model)
|
|
291
|
+
else:
|
|
292
|
+
raise ExecutorCreationError(
|
|
293
|
+
provider,
|
|
294
|
+
f"Unknown provider. Supported: {list(SUPPORTED_PROVIDERS)}",
|
|
295
|
+
)
|
|
296
|
+
except ProviderError:
|
|
297
|
+
raise
|
|
298
|
+
except Exception as e:
|
|
299
|
+
raise ExecutorCreationError(provider, str(e)) from e
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _create_claude_executor(
|
|
303
|
+
provider_config: "LLMProviderConfig | None",
|
|
304
|
+
model: str | None,
|
|
305
|
+
) -> AgentExecutor:
|
|
306
|
+
"""Create ClaudeExecutor with appropriate auth mode."""
|
|
307
|
+
from gobby.llm.claude_executor import ClaudeExecutor
|
|
308
|
+
|
|
309
|
+
# Determine auth mode and model from config
|
|
310
|
+
auth_mode = "api_key"
|
|
311
|
+
default_model = "claude-sonnet-4-20250514"
|
|
312
|
+
|
|
313
|
+
if provider_config:
|
|
314
|
+
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
315
|
+
# Get first model from comma-separated list if set
|
|
316
|
+
models_str = getattr(provider_config, "models", None)
|
|
317
|
+
if models_str:
|
|
318
|
+
models = [m.strip() for m in models_str.split(",") if m.strip()]
|
|
319
|
+
if models:
|
|
320
|
+
default_model = models[0]
|
|
321
|
+
|
|
322
|
+
return ClaudeExecutor(
|
|
323
|
+
auth_mode=auth_mode, # type: ignore[arg-type]
|
|
324
|
+
default_model=model or default_model,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _create_gemini_executor(
|
|
329
|
+
provider_config: "LLMProviderConfig | None",
|
|
330
|
+
model: str | None,
|
|
331
|
+
) -> AgentExecutor:
|
|
332
|
+
"""Create GeminiExecutor with appropriate auth mode."""
|
|
333
|
+
from gobby.llm.gemini_executor import GeminiExecutor
|
|
334
|
+
|
|
335
|
+
# Determine auth mode and model from config
|
|
336
|
+
auth_mode = "api_key"
|
|
337
|
+
default_model = "gemini-2.0-flash"
|
|
338
|
+
|
|
339
|
+
if provider_config:
|
|
340
|
+
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
341
|
+
models_str = getattr(provider_config, "models", None)
|
|
342
|
+
if models_str:
|
|
343
|
+
models = [m.strip() for m in models_str.split(",") if m.strip()]
|
|
344
|
+
if models:
|
|
345
|
+
default_model = models[0]
|
|
346
|
+
|
|
347
|
+
return GeminiExecutor(
|
|
348
|
+
auth_mode=auth_mode, # type: ignore[arg-type]
|
|
349
|
+
default_model=model or default_model,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _create_litellm_executor(
|
|
354
|
+
provider_config: "LLMProviderConfig | None",
|
|
355
|
+
config: "DaemonConfig | None",
|
|
356
|
+
model: str | None,
|
|
357
|
+
) -> AgentExecutor:
|
|
358
|
+
"""Create LiteLLMExecutor with API keys from config."""
|
|
359
|
+
from gobby.llm.litellm_executor import LiteLLMExecutor
|
|
360
|
+
|
|
361
|
+
# Determine model and API base from config
|
|
362
|
+
default_model = "gpt-4o-mini"
|
|
363
|
+
api_base = None
|
|
364
|
+
api_keys: dict[str, str] | None = None
|
|
365
|
+
|
|
366
|
+
if provider_config:
|
|
367
|
+
models_str = getattr(provider_config, "models", None)
|
|
368
|
+
if models_str:
|
|
369
|
+
models = [m.strip() for m in models_str.split(",") if m.strip()]
|
|
370
|
+
if models:
|
|
371
|
+
default_model = models[0]
|
|
372
|
+
api_base = getattr(provider_config, "api_base", None)
|
|
373
|
+
|
|
374
|
+
# Get API keys from llm_providers.api_keys
|
|
375
|
+
if config and config.llm_providers:
|
|
376
|
+
api_keys = config.llm_providers.api_keys or None
|
|
377
|
+
|
|
378
|
+
return LiteLLMExecutor(
|
|
379
|
+
default_model=model or default_model,
|
|
380
|
+
api_base=api_base,
|
|
381
|
+
api_keys=api_keys,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _create_codex_executor(
|
|
386
|
+
provider_config: "LLMProviderConfig | None",
|
|
387
|
+
model: str | None,
|
|
388
|
+
) -> AgentExecutor:
|
|
389
|
+
"""
|
|
390
|
+
Create CodexExecutor with appropriate auth mode.
|
|
391
|
+
|
|
392
|
+
Codex supports two modes with different capabilities:
|
|
393
|
+
- api_key: OpenAI API with function calling (full tool injection)
|
|
394
|
+
- subscription: Codex CLI with ChatGPT subscription (no custom tools)
|
|
395
|
+
|
|
396
|
+
See CodexExecutor docstring for detailed mode differences.
|
|
397
|
+
"""
|
|
398
|
+
from gobby.llm.codex_executor import CodexExecutor
|
|
399
|
+
|
|
400
|
+
# Determine auth mode and model from config
|
|
401
|
+
auth_mode = "api_key"
|
|
402
|
+
default_model = "gpt-4o"
|
|
403
|
+
|
|
404
|
+
if provider_config:
|
|
405
|
+
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
406
|
+
models_str = getattr(provider_config, "models", None)
|
|
407
|
+
if models_str:
|
|
408
|
+
models = [m.strip() for m in models_str.split(",") if m.strip()]
|
|
409
|
+
if models:
|
|
410
|
+
default_model = models[0]
|
|
411
|
+
|
|
412
|
+
return CodexExecutor(
|
|
413
|
+
auth_mode=auth_mode, # type: ignore[arg-type]
|
|
414
|
+
default_model=model or default_model,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# Re-export for TYPE_CHECKING
|
|
419
|
+
if TYPE_CHECKING:
|
|
420
|
+
from gobby.config.app import LLMProviderConfig
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class ExecutorRegistry:
|
|
424
|
+
"""
|
|
425
|
+
Registry for managing AgentExecutor instances.
|
|
426
|
+
|
|
427
|
+
Provides lazy initialization and caching of executors per provider.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
def __init__(self, config: "DaemonConfig | None" = None):
|
|
431
|
+
"""
|
|
432
|
+
Initialize ExecutorRegistry.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
config: Optional daemon config for provider settings.
|
|
436
|
+
"""
|
|
437
|
+
self._config = config
|
|
438
|
+
self._executors: dict[str, AgentExecutor] = {}
|
|
439
|
+
|
|
440
|
+
def get(
|
|
441
|
+
self,
|
|
442
|
+
provider: str | None = None,
|
|
443
|
+
workflow: "WorkflowDefinition | None" = None,
|
|
444
|
+
model: str | None = None,
|
|
445
|
+
) -> AgentExecutor:
|
|
446
|
+
"""
|
|
447
|
+
Get or create an executor for the resolved provider.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
provider: Optional explicit provider override.
|
|
451
|
+
workflow: Optional workflow with provider settings.
|
|
452
|
+
model: Optional model override.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
AgentExecutor instance.
|
|
456
|
+
|
|
457
|
+
Raises:
|
|
458
|
+
ProviderError: If provider resolution or creation fails.
|
|
459
|
+
"""
|
|
460
|
+
# Resolve provider
|
|
461
|
+
resolved = resolve_provider(
|
|
462
|
+
explicit_provider=provider,
|
|
463
|
+
workflow=workflow,
|
|
464
|
+
config=self._config,
|
|
465
|
+
allow_unconfigured=True, # Allow creating executors without full config
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Check cache
|
|
469
|
+
cache_key = f"{resolved.provider}:{model or resolved.model or 'default'}"
|
|
470
|
+
if cache_key in self._executors:
|
|
471
|
+
return self._executors[cache_key]
|
|
472
|
+
|
|
473
|
+
# Create executor
|
|
474
|
+
executor = create_executor(
|
|
475
|
+
provider=resolved.provider,
|
|
476
|
+
config=self._config,
|
|
477
|
+
model=model or resolved.model,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Cache and return
|
|
481
|
+
self._executors[cache_key] = executor
|
|
482
|
+
logger.info(
|
|
483
|
+
f"Created executor for provider '{resolved.provider}' (source={resolved.source})"
|
|
484
|
+
)
|
|
485
|
+
return executor
|
|
486
|
+
|
|
487
|
+
def get_all(self) -> dict[str, AgentExecutor]:
|
|
488
|
+
"""
|
|
489
|
+
Get all cached executors.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Dict mapping cache keys to executor instances.
|
|
493
|
+
"""
|
|
494
|
+
return dict(self._executors)
|
|
495
|
+
|
|
496
|
+
def clear_cache(self) -> None:
|
|
497
|
+
"""Clear the executor cache."""
|
|
498
|
+
self._executors.clear()
|
|
499
|
+
logger.debug("Cleared executor cache")
|