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.
Files changed (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. 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")