gobby 0.2.8__py3-none-any.whl → 0.2.11__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 (168) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +6 -0
  3. gobby/adapters/base.py +11 -2
  4. gobby/adapters/claude_code.py +5 -28
  5. gobby/adapters/codex_impl/adapter.py +38 -43
  6. gobby/adapters/copilot.py +324 -0
  7. gobby/adapters/cursor.py +373 -0
  8. gobby/adapters/gemini.py +2 -26
  9. gobby/adapters/windsurf.py +359 -0
  10. gobby/agents/definitions.py +162 -2
  11. gobby/agents/isolation.py +33 -1
  12. gobby/agents/pty_reader.py +192 -0
  13. gobby/agents/registry.py +10 -1
  14. gobby/agents/runner.py +24 -8
  15. gobby/agents/sandbox.py +8 -3
  16. gobby/agents/session.py +4 -0
  17. gobby/agents/spawn.py +9 -2
  18. gobby/agents/spawn_executor.py +49 -61
  19. gobby/agents/spawners/command_builder.py +4 -4
  20. gobby/app_context.py +64 -0
  21. gobby/cli/__init__.py +4 -0
  22. gobby/cli/install.py +259 -4
  23. gobby/cli/installers/__init__.py +12 -0
  24. gobby/cli/installers/copilot.py +242 -0
  25. gobby/cli/installers/cursor.py +244 -0
  26. gobby/cli/installers/shared.py +3 -0
  27. gobby/cli/installers/windsurf.py +242 -0
  28. gobby/cli/pipelines.py +639 -0
  29. gobby/cli/sessions.py +3 -1
  30. gobby/cli/skills.py +209 -0
  31. gobby/cli/tasks/crud.py +6 -5
  32. gobby/cli/tasks/search.py +1 -1
  33. gobby/cli/ui.py +116 -0
  34. gobby/cli/utils.py +5 -17
  35. gobby/cli/workflows.py +38 -17
  36. gobby/config/app.py +5 -0
  37. gobby/config/features.py +0 -20
  38. gobby/config/skills.py +23 -2
  39. gobby/config/tasks.py +4 -0
  40. gobby/hooks/broadcaster.py +9 -0
  41. gobby/hooks/event_handlers/__init__.py +155 -0
  42. gobby/hooks/event_handlers/_agent.py +175 -0
  43. gobby/hooks/event_handlers/_base.py +92 -0
  44. gobby/hooks/event_handlers/_misc.py +66 -0
  45. gobby/hooks/event_handlers/_session.py +487 -0
  46. gobby/hooks/event_handlers/_tool.py +196 -0
  47. gobby/hooks/events.py +48 -0
  48. gobby/hooks/hook_manager.py +27 -3
  49. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  50. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  51. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  52. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  53. gobby/llm/__init__.py +14 -1
  54. gobby/llm/claude.py +594 -43
  55. gobby/llm/service.py +149 -0
  56. gobby/mcp_proxy/importer.py +4 -41
  57. gobby/mcp_proxy/instructions.py +9 -27
  58. gobby/mcp_proxy/manager.py +13 -3
  59. gobby/mcp_proxy/models.py +1 -0
  60. gobby/mcp_proxy/registries.py +66 -5
  61. gobby/mcp_proxy/server.py +6 -2
  62. gobby/mcp_proxy/services/recommendation.py +2 -28
  63. gobby/mcp_proxy/services/tool_filter.py +7 -0
  64. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  65. gobby/mcp_proxy/stdio.py +37 -21
  66. gobby/mcp_proxy/tools/agents.py +7 -0
  67. gobby/mcp_proxy/tools/artifacts.py +3 -3
  68. gobby/mcp_proxy/tools/hub.py +30 -1
  69. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  70. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  71. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  72. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  73. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  74. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  75. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  76. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  77. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  78. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  79. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  80. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  81. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  82. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  83. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  84. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  85. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  86. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  87. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  88. gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
  89. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  90. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  91. gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
  92. gobby/mcp_proxy/tools/workflows/_query.py +226 -0
  93. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  94. gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
  95. gobby/mcp_proxy/tools/worktrees.py +54 -15
  96. gobby/memory/components/__init__.py +0 -0
  97. gobby/memory/components/ingestion.py +98 -0
  98. gobby/memory/components/search.py +108 -0
  99. gobby/memory/context.py +5 -5
  100. gobby/memory/manager.py +16 -25
  101. gobby/paths.py +51 -0
  102. gobby/prompts/loader.py +1 -35
  103. gobby/runner.py +131 -16
  104. gobby/servers/http.py +193 -150
  105. gobby/servers/routes/__init__.py +2 -0
  106. gobby/servers/routes/admin.py +56 -0
  107. gobby/servers/routes/mcp/endpoints/execution.py +33 -32
  108. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  109. gobby/servers/routes/mcp/hooks.py +10 -1
  110. gobby/servers/routes/pipelines.py +227 -0
  111. gobby/servers/websocket.py +314 -1
  112. gobby/sessions/analyzer.py +89 -3
  113. gobby/sessions/manager.py +5 -5
  114. gobby/sessions/transcripts/__init__.py +3 -0
  115. gobby/sessions/transcripts/claude.py +5 -0
  116. gobby/sessions/transcripts/codex.py +5 -0
  117. gobby/sessions/transcripts/gemini.py +5 -0
  118. gobby/skills/hubs/__init__.py +25 -0
  119. gobby/skills/hubs/base.py +234 -0
  120. gobby/skills/hubs/claude_plugins.py +328 -0
  121. gobby/skills/hubs/clawdhub.py +289 -0
  122. gobby/skills/hubs/github_collection.py +465 -0
  123. gobby/skills/hubs/manager.py +263 -0
  124. gobby/skills/hubs/skillhub.py +342 -0
  125. gobby/skills/parser.py +23 -0
  126. gobby/skills/sync.py +5 -4
  127. gobby/storage/artifacts.py +19 -0
  128. gobby/storage/memories.py +4 -4
  129. gobby/storage/migrations.py +118 -3
  130. gobby/storage/pipelines.py +367 -0
  131. gobby/storage/sessions.py +23 -4
  132. gobby/storage/skills.py +48 -8
  133. gobby/storage/tasks/_aggregates.py +2 -2
  134. gobby/storage/tasks/_lifecycle.py +4 -4
  135. gobby/storage/tasks/_models.py +7 -1
  136. gobby/storage/tasks/_queries.py +3 -3
  137. gobby/sync/memories.py +4 -3
  138. gobby/tasks/commits.py +48 -17
  139. gobby/tasks/external_validator.py +4 -17
  140. gobby/tasks/validation.py +13 -87
  141. gobby/tools/summarizer.py +18 -51
  142. gobby/utils/status.py +13 -0
  143. gobby/workflows/actions.py +80 -0
  144. gobby/workflows/context_actions.py +265 -27
  145. gobby/workflows/definitions.py +119 -1
  146. gobby/workflows/detection_helpers.py +23 -11
  147. gobby/workflows/enforcement/__init__.py +11 -1
  148. gobby/workflows/enforcement/blocking.py +96 -0
  149. gobby/workflows/enforcement/handlers.py +35 -1
  150. gobby/workflows/enforcement/task_policy.py +18 -0
  151. gobby/workflows/engine.py +26 -4
  152. gobby/workflows/evaluator.py +8 -5
  153. gobby/workflows/lifecycle_evaluator.py +59 -27
  154. gobby/workflows/loader.py +567 -30
  155. gobby/workflows/lobster_compat.py +147 -0
  156. gobby/workflows/pipeline_executor.py +801 -0
  157. gobby/workflows/pipeline_state.py +172 -0
  158. gobby/workflows/pipeline_webhooks.py +206 -0
  159. gobby/workflows/premature_stop.py +5 -0
  160. gobby/worktrees/git.py +135 -20
  161. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  162. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
  163. gobby/hooks/event_handlers.py +0 -1008
  164. gobby/mcp_proxy/tools/workflows.py +0 -1023
  165. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  166. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  167. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  168. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/servers/http.py CHANGED
@@ -10,7 +10,7 @@ import logging
10
10
  import time
11
11
  from collections.abc import AsyncGenerator
12
12
  from contextlib import asynccontextmanager
13
- from typing import Any
13
+ from typing import TYPE_CHECKING, Any
14
14
 
15
15
  from fastapi import FastAPI, HTTPException, Request
16
16
  from fastapi.middleware.cors import CORSMiddleware
@@ -19,18 +19,22 @@ from fastapi.responses import JSONResponse
19
19
  from gobby.adapters.codex_impl.adapter import CodexAdapter
20
20
  from gobby.hooks.broadcaster import HookEventBroadcaster
21
21
  from gobby.hooks.hook_manager import HookManager
22
- from gobby.llm import LLMService, create_llm_service
22
+ from gobby.llm import create_llm_service
23
23
  from gobby.mcp_proxy.registries import setup_internal_registries
24
24
  from gobby.mcp_proxy.semantic_search import SemanticToolSearch
25
25
  from gobby.mcp_proxy.server import GobbyDaemonTools, create_mcp_server
26
26
  from gobby.mcp_proxy.services.tool_filter import ToolFilterService
27
- from gobby.memory.manager import MemoryManager
28
- from gobby.storage.sessions import LocalSessionManager
29
- from gobby.storage.tasks import LocalTaskManager
30
- from gobby.sync.tasks import TaskSyncManager
31
27
  from gobby.utils.metrics import get_metrics_collector
32
28
  from gobby.utils.version import get_version
33
29
 
30
+ if TYPE_CHECKING:
31
+ from gobby.app_context import ServiceContainer
32
+ from gobby.config.app import DaemonConfig
33
+ from gobby.llm import LLMService
34
+ from gobby.mcp_proxy.manager import MCPClientManager
35
+ from gobby.servers.websocket import WebSocketServer
36
+ from gobby.utils.tool_metrics import ToolMetricsManager
37
+
34
38
  logger = logging.getLogger(__name__)
35
39
 
36
40
 
@@ -44,84 +48,38 @@ class HTTPServer:
44
48
 
45
49
  def __init__(
46
50
  self,
51
+ services: "ServiceContainer",
47
52
  port: int = 8000,
48
53
  test_mode: bool = False,
49
- mcp_manager: Any | None = None,
50
- mcp_db_manager: Any | None = None,
51
- config: Any | None = None,
52
54
  codex_client: Any | None = None,
53
- session_manager: LocalSessionManager | None = None,
54
- websocket_server: Any | None = None,
55
- task_manager: LocalTaskManager | None = None,
56
- task_sync_manager: TaskSyncManager | None = None,
57
- message_processor: Any | None = None,
58
- message_manager: Any | None = None, # LocalSessionMessageManager
59
- memory_manager: "MemoryManager | None" = None,
60
- llm_service: "LLMService | None" = None,
61
- memory_sync_manager: Any | None = None,
62
- task_validator: Any | None = None,
63
- metrics_manager: Any | None = None,
64
- agent_runner: Any | None = None,
65
- worktree_storage: Any | None = None,
66
- clone_storage: Any | None = None,
67
- git_manager: Any | None = None,
68
- project_id: str | None = None,
69
55
  ) -> None:
70
56
  """
71
57
  Initialize HTTP server.
72
58
 
73
59
  Args:
60
+ services: ServiceContainer holding all dependencies
74
61
  port: Server port
75
62
  test_mode: Run in test mode (disable features that conflict with testing)
76
- mcp_manager: MCPClientManager instance for multi-server support
77
- mcp_db_manager: LocalMCPManager instance for SQLite-based storage of MCP
78
- server configurations and tool schemas. Used by ToolsHandler for
79
- progressive tool discovery. Optional; defaults to None.
80
- config: DaemonConfig instance for configuration
81
63
  codex_client: CodexAppServerClient instance for Codex integration
82
- session_manager: LocalSessionManager for session storage
83
- websocket_server: Optional WebSocketServer instance for event broadcasting
84
- task_manager: LocalTaskManager instance
85
- task_sync_manager: TaskSyncManager instance
86
- message_processor: SessionMessageProcessor instance
87
- message_manager: LocalSessionMessageManager instance for retrieval
88
- memory_manager: MemoryManager instance
89
- llm_service: LLMService instance
90
64
  """
65
+ self.services = services
91
66
  self.port = port
92
67
  self.test_mode = test_mode
93
- self.mcp_manager = mcp_manager
94
- self.config = config
95
68
  self.codex_client = codex_client
96
- self.session_manager = session_manager
97
- self.task_manager = task_manager
98
- self.task_sync_manager = task_sync_manager
99
- self.message_processor = message_processor
100
- self.message_manager = message_manager
101
- self.memory_manager = memory_manager
102
- self.websocket_server = websocket_server
103
- self.llm_service = llm_service
104
- self.memory_sync_manager = memory_sync_manager
105
- self.task_validator = task_validator
106
- self.metrics_manager = metrics_manager
107
- self.agent_runner = agent_runner
108
- self.worktree_storage = worktree_storage
109
- self.clone_storage = clone_storage
110
- self.git_manager = git_manager
111
- self.project_id = project_id
112
-
113
- # Initialize WebSocket broadcaster
114
- # Note: websocket_server might be None if disabled
115
- self.broadcaster = HookEventBroadcaster(websocket_server, config)
69
+
70
+ # WebSocket server reference (set by GobbyRunner after construction)
71
+ self.websocket_server: WebSocketServer | None = None
72
+
73
+ self.broadcaster = HookEventBroadcaster(services.websocket_server, services.config)
116
74
 
117
75
  self._start_time: float = time.time()
118
76
 
119
- # Create LLM service if not provided
120
- if not self.llm_service and config:
77
+ # Create LLM service if not provided in container (fallback)
78
+ if not services.llm_service and services.config:
121
79
  try:
122
- self.llm_service = create_llm_service(config)
80
+ services.llm_service = create_llm_service(services.config)
123
81
  logger.debug(
124
- f"LLM service initialized with providers: {self.llm_service.enabled_providers}"
82
+ f"LLM service initialized with providers: {services.llm_service.enabled_providers}"
125
83
  )
126
84
  except Exception as e:
127
85
  logger.error(f"Failed to initialize LLM service: {e}")
@@ -130,12 +88,13 @@ class HTTPServer:
130
88
  self._mcp_server = None
131
89
  self._internal_manager = None
132
90
  self._tools_handler = None
133
- self._mcp_db_manager = mcp_db_manager
134
- if mcp_manager:
91
+
92
+ if services.mcp_manager:
135
93
  # Determine WebSocket port
136
94
  ws_port = 60888
137
- if config and hasattr(config, "websocket") and config.websocket:
138
- ws_port = config.websocket.port
95
+ cfg = services.config
96
+ if cfg and hasattr(cfg, "websocket") and cfg.websocket:
97
+ ws_port = cfg.websocket.port
139
98
 
140
99
  # Create a lazy getter for tool_proxy that will be available after
141
100
  # GobbyDaemonTools is created. This allows in-process agents to route
@@ -149,84 +108,90 @@ class HTTPServer:
149
108
  merge_storage = None
150
109
  merge_resolver = None
151
110
  inter_session_message_manager = None
152
- if mcp_db_manager:
111
+ if self.services.mcp_db_manager:
153
112
  from gobby.storage.inter_session_messages import InterSessionMessageManager
154
113
  from gobby.storage.merge_resolutions import MergeResolutionManager
155
114
  from gobby.worktrees.merge.resolver import MergeResolver
156
115
 
157
- merge_storage = MergeResolutionManager(mcp_db_manager.db)
116
+ merge_storage = MergeResolutionManager(self.services.mcp_db_manager.db)
158
117
  merge_resolver = MergeResolver()
159
- merge_resolver._llm_service = self.llm_service
160
- inter_session_message_manager = InterSessionMessageManager(mcp_db_manager.db)
118
+ merge_resolver._llm_service = services.llm_service
119
+ inter_session_message_manager = InterSessionMessageManager(
120
+ self.services.mcp_db_manager.db
121
+ )
161
122
  logger.debug("Merge resolution and inter-session messaging subsystems initialized")
162
123
 
163
- # Setup internal registries (gobby-tasks, gobby-memory, etc.)
124
+ # Setup internal registries (gobby-tasks, gobby-memory, gobby-pipelines, etc.)
164
125
  self._internal_manager = setup_internal_registries(
165
- _config=config,
126
+ _config=services.config,
166
127
  _session_manager=None, # Not needed for internal registries
167
- memory_manager=memory_manager,
168
- task_manager=task_manager,
169
- sync_manager=task_sync_manager,
170
- task_validator=self.task_validator,
171
- message_manager=message_manager,
172
- local_session_manager=session_manager,
173
- metrics_manager=self.metrics_manager,
174
- llm_service=self.llm_service,
175
- agent_runner=self.agent_runner,
176
- worktree_storage=self.worktree_storage,
177
- clone_storage=self.clone_storage,
178
- git_manager=self.git_manager,
128
+ memory_manager=services.memory_manager,
129
+ task_manager=services.task_manager,
130
+ db=services.mcp_db_manager.db if services.mcp_db_manager else None,
131
+ sync_manager=services.task_sync_manager,
132
+ task_validator=services.task_validator,
133
+ message_manager=services.message_manager,
134
+ local_session_manager=services.session_manager,
135
+ metrics_manager=services.metrics_manager,
136
+ llm_service=services.llm_service,
137
+ agent_runner=services.agent_runner,
138
+ worktree_storage=services.worktree_storage,
139
+ clone_storage=services.clone_storage,
140
+ git_manager=services.git_manager,
179
141
  merge_storage=merge_storage,
180
142
  merge_resolver=merge_resolver,
181
- project_id=self.project_id,
143
+ project_id=services.project_id,
182
144
  tool_proxy_getter=tool_proxy_getter,
183
145
  inter_session_message_manager=inter_session_message_manager,
146
+ pipeline_executor=services.pipeline_executor,
147
+ workflow_loader=services.workflow_loader,
148
+ pipeline_execution_manager=services.pipeline_execution_manager,
184
149
  )
185
150
  registry_count = len(self._internal_manager)
186
151
  logger.debug(f"Internal registries initialized: {registry_count} registries")
187
152
 
188
153
  # Initialize tool summarizer config
189
- if config:
154
+ if services.config:
190
155
  from gobby.tools.summarizer import init_summarizer_config
191
156
 
192
- init_summarizer_config(config.tool_summarizer)
157
+ init_summarizer_config(services.config.tool_summarizer)
193
158
  logger.debug("Tool summarizer config initialized")
194
159
 
195
160
  # Create semantic search instance if db available
196
161
  semantic_search = None
197
- if mcp_db_manager:
198
- semantic_search = SemanticToolSearch(db=mcp_db_manager.db)
162
+ if services.mcp_db_manager:
163
+ semantic_search = SemanticToolSearch(db=services.mcp_db_manager.db)
199
164
  logger.debug("Semantic tool search initialized")
200
165
 
201
166
  # Create tool filter for workflow phase restrictions
202
167
  tool_filter = None
203
- if mcp_db_manager:
204
- tool_filter = ToolFilterService(db=mcp_db_manager.db)
168
+ if services.mcp_db_manager:
169
+ tool_filter = ToolFilterService(db=services.mcp_db_manager.db)
205
170
  logger.debug("Tool filter service initialized")
206
171
 
207
172
  # Create fallback resolver for alternative tool suggestions on error
208
173
  fallback_resolver = None
209
- if semantic_search and self.metrics_manager:
174
+ if semantic_search and services.metrics_manager:
210
175
  from gobby.mcp_proxy.services.fallback import ToolFallbackResolver
211
176
 
212
177
  fallback_resolver = ToolFallbackResolver(
213
178
  semantic_search=semantic_search,
214
- metrics_manager=self.metrics_manager,
179
+ metrics_manager=services.metrics_manager,
215
180
  )
216
181
  logger.debug("Fallback resolver initialized")
217
182
 
218
183
  # Create tools handler
219
184
  self._tools_handler = GobbyDaemonTools(
220
- mcp_manager=mcp_manager,
185
+ mcp_manager=services.mcp_manager,
221
186
  daemon_port=port,
222
187
  websocket_port=ws_port,
223
188
  start_time=self._start_time,
224
189
  internal_manager=self._internal_manager,
225
- config=config,
226
- llm_service=self.llm_service,
227
- session_manager=session_manager,
228
- memory_manager=memory_manager,
229
- config_manager=mcp_db_manager,
190
+ config=services.config,
191
+ llm_service=services.llm_service,
192
+ session_manager=services.session_manager,
193
+ memory_manager=services.memory_manager,
194
+ config_manager=services.mcp_db_manager,
230
195
  semantic_search=semantic_search,
231
196
  tool_filter=tool_filter,
232
197
  fallback_resolver=fallback_resolver,
@@ -240,6 +205,83 @@ class HTTPServer:
240
205
  self._metrics = get_metrics_collector()
241
206
  self._daemon: Any = None # Set externally by daemon
242
207
 
208
+ # Property accessors for services (delegate to container)
209
+ @property
210
+ def config(self) -> "DaemonConfig | None":
211
+ return self.services.config
212
+
213
+ @property
214
+ def session_manager(self) -> Any:
215
+ return self.services.session_manager
216
+
217
+ @session_manager.setter
218
+ def session_manager(self, value: Any) -> None:
219
+ self.services.session_manager = value
220
+
221
+ @property
222
+ def task_manager(self) -> Any:
223
+ return self.services.task_manager
224
+
225
+ @task_manager.setter
226
+ def task_manager(self, value: Any) -> None:
227
+ self.services.task_manager = value
228
+
229
+ @property
230
+ def mcp_manager(self) -> "MCPClientManager | None":
231
+ return self.services.mcp_manager
232
+
233
+ @mcp_manager.setter
234
+ def mcp_manager(self, value: "MCPClientManager | None") -> None:
235
+ self.services.mcp_manager = value
236
+
237
+ @property
238
+ def llm_service(self) -> "LLMService | None":
239
+ return self.services.llm_service
240
+
241
+ @llm_service.setter
242
+ def llm_service(self, value: "LLMService | None") -> None:
243
+ self.services.llm_service = value
244
+
245
+ @property
246
+ def memory_manager(self) -> Any:
247
+ return self.services.memory_manager
248
+
249
+ @memory_manager.setter
250
+ def memory_manager(self, value: Any) -> None:
251
+ self.services.memory_manager = value
252
+
253
+ @property
254
+ def message_manager(self) -> Any:
255
+ return self.services.message_manager
256
+
257
+ @message_manager.setter
258
+ def message_manager(self, value: Any) -> None:
259
+ self.services.message_manager = value
260
+
261
+ @property
262
+ def message_processor(self) -> Any:
263
+ return self.services.message_processor
264
+
265
+ @message_processor.setter
266
+ def message_processor(self, value: Any) -> None:
267
+ self.services.message_processor = value
268
+
269
+ @property
270
+ def metrics_manager(self) -> "ToolMetricsManager | None":
271
+ return self.services.metrics_manager
272
+
273
+ @metrics_manager.setter
274
+ def metrics_manager(self, value: "ToolMetricsManager | None") -> None:
275
+ self.services.metrics_manager = value
276
+
277
+ @property
278
+ def _mcp_db_manager(self) -> Any:
279
+ return self.services.mcp_db_manager
280
+
281
+ @_mcp_db_manager.setter
282
+ def _mcp_db_manager(self, value: Any) -> None:
283
+ self.services.mcp_db_manager = value
284
+
243
285
  @property
244
286
  def tool_proxy(self) -> Any:
245
287
  """Get the ToolProxyService instance for routing tool calls with error enrichment."""
@@ -314,30 +356,32 @@ class HTTPServer:
314
356
  hook_manager_kwargs: dict[str, Any] = {
315
357
  "daemon_host": "localhost",
316
358
  "daemon_port": self.port,
317
- "llm_service": self.llm_service,
318
- "config": self.config,
359
+ "llm_service": self.services.llm_service,
360
+ "config": self.services.config,
319
361
  "broadcaster": self.broadcaster,
320
- "mcp_manager": self.mcp_manager,
321
- "message_processor": self.message_processor,
322
- "memory_sync_manager": self.memory_sync_manager,
323
- "task_sync_manager": self.task_sync_manager,
362
+ "mcp_manager": self.services.mcp_manager,
363
+ "message_processor": self.services.message_processor,
364
+ "memory_sync_manager": self.services.memory_sync_manager,
365
+ "task_sync_manager": self.services.task_sync_manager,
324
366
  }
325
- if self.config:
367
+ if self.services.config:
326
368
  # Pass full log file path from config
327
- hook_manager_kwargs["log_file"] = self.config.logging.hook_manager
328
- hook_manager_kwargs["log_max_bytes"] = self.config.logging.max_size_mb * 1024 * 1024
329
- hook_manager_kwargs["log_backup_count"] = self.config.logging.backup_count
369
+ hook_manager_kwargs["log_file"] = self.services.config.logging.hook_manager
370
+ hook_manager_kwargs["log_max_bytes"] = (
371
+ self.services.config.logging.max_size_mb * 1024 * 1024
372
+ )
373
+ hook_manager_kwargs["log_backup_count"] = self.services.config.logging.backup_count
330
374
 
331
375
  app.state.hook_manager = HookManager(**hook_manager_kwargs)
332
376
  logger.debug("HookManager initialized in daemon")
333
377
 
334
378
  # Wire up stop_registry to WebSocket server for stop_request handling
335
379
  if (
336
- self.websocket_server
380
+ self.services.websocket_server
337
381
  and hasattr(app.state, "hook_manager")
338
382
  and hasattr(app.state.hook_manager, "_stop_registry")
339
383
  ):
340
- self.websocket_server.stop_registry = app.state.hook_manager._stop_registry
384
+ self.services.websocket_server.stop_registry = app.state.hook_manager._stop_registry
341
385
  logger.debug("Stop registry connected to WebSocket server")
342
386
 
343
387
  # Store server instance for dependency injection
@@ -474,6 +518,7 @@ class HTTPServer:
474
518
  create_admin_router,
475
519
  create_hooks_router,
476
520
  create_mcp_router,
521
+ create_pipelines_router,
477
522
  create_plugins_router,
478
523
  create_sessions_router,
479
524
  create_webhooks_router,
@@ -486,6 +531,7 @@ class HTTPServer:
486
531
  app.include_router(create_hooks_router(self))
487
532
  app.include_router(create_plugins_router())
488
533
  app.include_router(create_webhooks_router())
534
+ app.include_router(create_pipelines_router(self))
489
535
 
490
536
  async def _process_shutdown(self) -> None:
491
537
  """
@@ -496,45 +542,48 @@ class HTTPServer:
496
542
  try:
497
543
  logger.debug("Processing graceful shutdown")
498
544
 
499
- # Wait for pending background tasks to complete
545
+ # Cancel pending background tasks immediately instead of polling
500
546
  pending_tasks_count = len(self._background_tasks)
501
547
  if pending_tasks_count > 0:
502
548
  logger.debug(
503
- "Waiting for pending background tasks to complete",
549
+ "Cancelling pending background tasks",
504
550
  extra={"pending_tasks": pending_tasks_count},
505
551
  )
506
552
 
507
- max_wait = 30.0
508
- wait_start = time.perf_counter()
509
-
510
- while (
511
- len(self._background_tasks) > 0
512
- and (time.perf_counter() - wait_start) < max_wait
513
- ):
514
- await asyncio.sleep(0.5)
515
-
516
- completed_wait = time.perf_counter() - wait_start
517
- remaining_tasks = len(self._background_tasks)
518
-
519
- if remaining_tasks > 0:
520
- logger.warning(
521
- "Shutdown timeout - some background tasks still pending",
522
- extra={
523
- "remaining_tasks": remaining_tasks,
524
- "wait_seconds": completed_wait,
525
- },
526
- )
527
- else:
528
- logger.debug(
529
- "All background tasks completed",
530
- extra={"wait_seconds": completed_wait},
553
+ # Cancel all tasks
554
+ for task in self._background_tasks:
555
+ task.cancel()
556
+
557
+ # Wait for cancellation to complete with a short timeout
558
+ if self._background_tasks:
559
+ done, pending = await asyncio.wait(
560
+ self._background_tasks,
561
+ timeout=5.0,
562
+ return_when=asyncio.ALL_COMPLETED,
531
563
  )
532
564
 
565
+ completed_count = len(done)
566
+ remaining_count = len(pending)
567
+
568
+ if remaining_count > 0:
569
+ logger.warning(
570
+ "Some background tasks did not cancel in time",
571
+ extra={
572
+ "completed": completed_count,
573
+ "remaining": remaining_count,
574
+ },
575
+ )
576
+ else:
577
+ logger.debug(
578
+ "All background tasks cancelled",
579
+ extra={"completed": completed_count},
580
+ )
581
+
533
582
  # Disconnect all MCP servers
534
- if self.mcp_manager:
583
+ if self.services.mcp_manager:
535
584
  logger.debug("Disconnecting MCP servers...")
536
585
  try:
537
- await self.mcp_manager.disconnect_all()
586
+ await self.services.mcp_manager.disconnect_all()
538
587
  logger.debug("MCP servers disconnected")
539
588
  except Exception as e:
540
589
  logger.warning(f"Error disconnecting MCP servers: {e}")
@@ -560,31 +609,25 @@ class HTTPServer:
560
609
 
561
610
 
562
611
  async def create_server(
612
+ services: "ServiceContainer",
563
613
  port: int = 60887,
564
614
  test_mode: bool = False,
565
- mcp_manager: Any | None = None,
566
- config: Any | None = None,
567
- session_manager: LocalSessionManager | None = None,
568
615
  ) -> HTTPServer:
569
616
  """
570
617
  Create HTTP server instance.
571
618
 
572
619
  Args:
620
+ services: ServiceContainer holding dependencies
573
621
  port: Port to listen on
574
622
  test_mode: Enable test mode
575
- mcp_manager: MCP client manager
576
- config: Daemon configuration
577
- session_manager: Local session manager
578
623
 
579
624
  Returns:
580
625
  Configured HTTPServer instance
581
626
  """
582
627
  return HTTPServer(
628
+ services=services,
583
629
  port=port,
584
630
  test_mode=test_mode,
585
- mcp_manager=mcp_manager,
586
- config=config,
587
- session_manager=session_manager,
588
631
  )
589
632
 
590
633
 
@@ -11,12 +11,14 @@ from gobby.servers.routes.mcp import (
11
11
  create_plugins_router,
12
12
  create_webhooks_router,
13
13
  )
14
+ from gobby.servers.routes.pipelines import create_pipelines_router
14
15
  from gobby.servers.routes.sessions import create_sessions_router
15
16
 
16
17
  __all__ = [
17
18
  "create_admin_router",
18
19
  "create_hooks_router",
19
20
  "create_mcp_router",
21
+ "create_pipelines_router",
20
22
  "create_plugins_router",
21
23
  "create_sessions_router",
22
24
  "create_webhooks_router",
@@ -175,6 +175,17 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
175
175
  except Exception as e:
176
176
  logger.warning(f"Failed to get memory stats: {e}")
177
177
 
178
+ # Get artifact statistics
179
+ artifact_stats = {"count": 0}
180
+ if server.session_manager is not None:
181
+ try:
182
+ from gobby.storage.artifacts import LocalArtifactManager
183
+
184
+ artifact_manager = LocalArtifactManager(server.session_manager.db)
185
+ artifact_stats["count"] = artifact_manager.count_artifacts()
186
+ except Exception as e:
187
+ logger.warning(f"Failed to get artifact stats: {e}")
188
+
178
189
  # Get skills statistics
179
190
  skills_stats: dict[str, Any] = {"total": 0}
180
191
  if server._internal_manager:
@@ -231,6 +242,7 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
231
242
  "sessions": session_stats,
232
243
  "tasks": task_stats,
233
244
  "memory": memory_stats,
245
+ "artifacts": artifact_stats,
234
246
  "skills": skills_stats,
235
247
  "plugins": plugin_stats,
236
248
  "response_time_ms": response_time_ms,
@@ -278,6 +290,50 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
278
290
  logger.error(f"Failed to export metrics: {e}", exc_info=True)
279
291
  raise
280
292
 
293
+ @router.get("/models")
294
+ async def get_models() -> dict[str, Any]:
295
+ """
296
+ Get available LLM providers and their models.
297
+
298
+ Returns:
299
+ Dictionary with providers and their available models
300
+ """
301
+ start_time = time.perf_counter()
302
+
303
+ providers_data: dict[str, Any] = {}
304
+ default_provider = None
305
+ default_model = None
306
+
307
+ if server.llm_service is not None:
308
+ enabled = server.llm_service.enabled_providers
309
+ if enabled:
310
+ default_provider = "claude" if "claude" in enabled else enabled[0]
311
+
312
+ # Get models for each enabled provider from config
313
+ if server.services.config and server.services.config.llm_providers:
314
+ llm_config = server.services.config.llm_providers
315
+
316
+ for provider_name in enabled:
317
+ provider_config = getattr(llm_config, provider_name, None)
318
+ if provider_config:
319
+ models = provider_config.get_models_list()
320
+ providers_data[provider_name] = {
321
+ "models": models,
322
+ "auth_mode": provider_config.auth_mode,
323
+ }
324
+ # Set default model from first provider
325
+ if default_model is None and models:
326
+ default_model = models[0]
327
+
328
+ response_time_ms = (time.perf_counter() - start_time) * 1000
329
+
330
+ return {
331
+ "providers": providers_data,
332
+ "default_provider": default_provider,
333
+ "default_model": default_model,
334
+ "response_time_ms": response_time_ms,
335
+ }
336
+
281
337
  @router.get("/config")
282
338
  async def get_config() -> dict[str, Any]:
283
339
  """