gobby 0.2.7__py3-none-any.whl → 0.2.9__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 +1 -1
- gobby/adapters/claude_code.py +99 -61
- gobby/adapters/gemini.py +140 -38
- gobby/agents/isolation.py +130 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn_executor.py +43 -13
- gobby/agents/spawners/macos.py +26 -1
- gobby/app_context.py +59 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/memory.py +185 -0
- gobby/cli/utils.py +5 -17
- gobby/clones/git.py +177 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +87 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +573 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/hook_manager.py +21 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/claude.py +377 -42
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +2 -2
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/registries.py +35 -4
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +45 -9
- gobby/mcp_proxy/tools/artifacts.py +46 -12
- gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
- gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
- gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
- gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
- gobby/mcp_proxy/tools/spawn_agent.py +44 -6
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
- gobby/mcp_proxy/tools/workflows/_query.py +207 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
- gobby/mcp_proxy/tools/worktrees.py +32 -7
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/extractor.py +15 -1
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +36 -10
- gobby/servers/http.py +186 -149
- gobby/servers/routes/admin.py +12 -0
- gobby/servers/routes/mcp/endpoints/execution.py +15 -7
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +50 -3
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +4 -4
- gobby/sessions/manager.py +9 -0
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +46 -4
- gobby/storage/sessions.py +4 -2
- gobby/storage/skills.py +87 -7
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +5 -0
- gobby/workflows/context_actions.py +21 -24
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +109 -1
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/engine.py +96 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/lifecycle_evaluator.py +2 -1
- gobby/workflows/memory_actions.py +11 -0
- gobby/workflows/safe_evaluator.py +8 -0
- gobby/workflows/summary_actions.py +123 -50
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/RECORD +99 -107
- gobby/cli/tui.py +0 -34
- gobby/hooks/event_handlers.py +0 -909
- gobby/mcp_proxy/tools/workflows.py +0 -973
- gobby/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/top_level.txt +0 -0
gobby/prompts/loader.py
CHANGED
|
@@ -12,7 +12,6 @@ Implements prompt loading with precedence:
|
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
14
|
import re
|
|
15
|
-
from collections.abc import Callable
|
|
16
15
|
from functools import lru_cache
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from typing import Any
|
|
@@ -66,21 +65,6 @@ class PromptLoader:
|
|
|
66
65
|
# Template cache
|
|
67
66
|
self._cache: dict[str, PromptTemplate] = {}
|
|
68
67
|
|
|
69
|
-
# Fallback registry for strangler fig pattern
|
|
70
|
-
self._fallbacks: dict[str, Callable[[], str]] = {}
|
|
71
|
-
|
|
72
|
-
def register_fallback(self, path: str, getter: Callable[[], str]) -> None:
|
|
73
|
-
"""Register a Python constant fallback for a template path.
|
|
74
|
-
|
|
75
|
-
Used for strangler fig pattern - if template file doesn't exist,
|
|
76
|
-
fall back to the original Python constant.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
path: Template path (e.g., "expansion/system")
|
|
80
|
-
getter: Callable that returns the fallback string
|
|
81
|
-
"""
|
|
82
|
-
self._fallbacks[path] = getter
|
|
83
|
-
|
|
84
68
|
def clear_cache(self) -> None:
|
|
85
69
|
"""Clear the template cache."""
|
|
86
70
|
self._cache.clear()
|
|
@@ -162,19 +146,6 @@ class PromptLoader:
|
|
|
162
146
|
logger.debug(f"Loaded prompt template '{path}' from {template_file}")
|
|
163
147
|
return template
|
|
164
148
|
|
|
165
|
-
# Fall back to registered Python constant
|
|
166
|
-
if path in self._fallbacks:
|
|
167
|
-
fallback_content = self._fallbacks[path]()
|
|
168
|
-
template = PromptTemplate(
|
|
169
|
-
name=path,
|
|
170
|
-
description=f"Fallback for {path}",
|
|
171
|
-
content=fallback_content,
|
|
172
|
-
source_path=None,
|
|
173
|
-
)
|
|
174
|
-
self._cache[path] = template
|
|
175
|
-
logger.debug(f"Using fallback for prompt template '{path}'")
|
|
176
|
-
return template
|
|
177
|
-
|
|
178
149
|
raise FileNotFoundError(f"Prompt template not found: {path}")
|
|
179
150
|
|
|
180
151
|
def render(
|
|
@@ -280,7 +251,7 @@ class PromptLoader:
|
|
|
280
251
|
Returns:
|
|
281
252
|
True if template exists (file or fallback)
|
|
282
253
|
"""
|
|
283
|
-
return self._find_template_file(path) is not None
|
|
254
|
+
return self._find_template_file(path) is not None
|
|
284
255
|
|
|
285
256
|
def list_templates(self, category: str | None = None) -> list[str]:
|
|
286
257
|
"""List available template paths.
|
|
@@ -305,11 +276,6 @@ class PromptLoader:
|
|
|
305
276
|
if category is None or template_path.startswith(f"{category}/"):
|
|
306
277
|
templates.add(template_path)
|
|
307
278
|
|
|
308
|
-
# Add registered fallbacks
|
|
309
|
-
for fallback_path in self._fallbacks:
|
|
310
|
-
if category is None or fallback_path.startswith(f"{category}/"):
|
|
311
|
-
templates.add(fallback_path)
|
|
312
|
-
|
|
313
279
|
return sorted(templates)
|
|
314
280
|
|
|
315
281
|
|
gobby/runner.py
CHANGED
|
@@ -9,6 +9,7 @@ from typing import Any
|
|
|
9
9
|
import uvicorn
|
|
10
10
|
|
|
11
11
|
from gobby.agents.runner import AgentRunner
|
|
12
|
+
from gobby.app_context import ServiceContainer
|
|
12
13
|
from gobby.config.app import load_config
|
|
13
14
|
from gobby.llm import LLMService, create_llm_service
|
|
14
15
|
from gobby.llm.resolver import ExecutorRegistry
|
|
@@ -212,29 +213,38 @@ class GobbyRunner:
|
|
|
212
213
|
)
|
|
213
214
|
|
|
214
215
|
# HTTP Server
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
test_mode=self.config.test_mode,
|
|
218
|
-
mcp_manager=self.mcp_proxy,
|
|
219
|
-
mcp_db_manager=self.mcp_db_manager,
|
|
216
|
+
# Bundle services into container
|
|
217
|
+
services = ServiceContainer(
|
|
220
218
|
config=self.config,
|
|
219
|
+
database=self.database,
|
|
221
220
|
session_manager=self.session_manager,
|
|
222
221
|
task_manager=self.task_manager,
|
|
223
222
|
task_sync_manager=self.task_sync_manager,
|
|
224
|
-
|
|
223
|
+
memory_sync_manager=self.memory_sync_manager,
|
|
225
224
|
memory_manager=self.memory_manager,
|
|
226
225
|
llm_service=self.llm_service,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
task_validator=self.task_validator,
|
|
226
|
+
mcp_manager=self.mcp_proxy,
|
|
227
|
+
mcp_db_manager=self.mcp_db_manager,
|
|
230
228
|
metrics_manager=self.metrics_manager,
|
|
231
229
|
agent_runner=self.agent_runner,
|
|
230
|
+
message_processor=self.message_processor,
|
|
231
|
+
message_manager=self.message_manager,
|
|
232
|
+
task_validator=self.task_validator,
|
|
232
233
|
worktree_storage=self.worktree_storage,
|
|
233
234
|
clone_storage=self.clone_storage,
|
|
234
235
|
git_manager=self.git_manager,
|
|
235
236
|
project_id=self.project_id,
|
|
236
237
|
)
|
|
237
238
|
|
|
239
|
+
self.http_server = HTTPServer(
|
|
240
|
+
services=services,
|
|
241
|
+
port=self.config.daemon_port,
|
|
242
|
+
test_mode=self.config.test_mode,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Inject server into container for circular ref if needed later
|
|
246
|
+
# self.http_server.services = services
|
|
247
|
+
|
|
238
248
|
# Ensure message_processor property is set (redundant but explicit):
|
|
239
249
|
self.http_server.message_processor = self.message_processor
|
|
240
250
|
|
|
@@ -424,12 +434,14 @@ class GobbyRunner:
|
|
|
424
434
|
|
|
425
435
|
# Start HTTP server
|
|
426
436
|
# nosec B104: 0.0.0.0 binding is intentional - daemon serves local network
|
|
437
|
+
graceful_shutdown_timeout = 15
|
|
427
438
|
config = uvicorn.Config(
|
|
428
439
|
self.http_server.app,
|
|
429
440
|
host="0.0.0.0", # nosec B104 - local daemon needs network access
|
|
430
441
|
port=self.http_server.port,
|
|
431
442
|
log_level="warning",
|
|
432
443
|
access_log=False,
|
|
444
|
+
timeout_graceful_shutdown=graceful_shutdown_timeout,
|
|
433
445
|
)
|
|
434
446
|
server = uvicorn.Server(config)
|
|
435
447
|
server_task = asyncio.create_task(server.serve())
|
|
@@ -439,9 +451,10 @@ class GobbyRunner:
|
|
|
439
451
|
await asyncio.sleep(0.5)
|
|
440
452
|
|
|
441
453
|
# Cleanup with timeouts to prevent hanging
|
|
454
|
+
# Use timeout slightly longer than uvicorn's graceful shutdown to let it finish
|
|
442
455
|
server.should_exit = True
|
|
443
456
|
try:
|
|
444
|
-
await asyncio.wait_for(server_task, timeout=
|
|
457
|
+
await asyncio.wait_for(server_task, timeout=graceful_shutdown_timeout + 5)
|
|
445
458
|
except TimeoutError:
|
|
446
459
|
logger.warning("HTTP server shutdown timed out")
|
|
447
460
|
|
|
@@ -471,6 +484,19 @@ class GobbyRunner:
|
|
|
471
484
|
except (asyncio.CancelledError, TimeoutError):
|
|
472
485
|
pass
|
|
473
486
|
|
|
487
|
+
# Export memories to JSONL backup on shutdown
|
|
488
|
+
if self.memory_sync_manager:
|
|
489
|
+
try:
|
|
490
|
+
count = await asyncio.wait_for(
|
|
491
|
+
self.memory_sync_manager.export_to_files(), timeout=5.0
|
|
492
|
+
)
|
|
493
|
+
if count > 0:
|
|
494
|
+
logger.info(f"Shutdown memory backup: exported {count} memories")
|
|
495
|
+
except TimeoutError:
|
|
496
|
+
logger.warning("Memory backup on shutdown timed out")
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logger.warning(f"Memory backup on shutdown failed: {e}")
|
|
499
|
+
|
|
474
500
|
try:
|
|
475
501
|
await asyncio.wait_for(self.mcp_proxy.disconnect_all(), timeout=3.0)
|
|
476
502
|
except TimeoutError:
|
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
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
self.
|
|
99
|
-
|
|
100
|
-
self.
|
|
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
|
|
77
|
+
# Create LLM service if not provided in container (fallback)
|
|
78
|
+
if not services.llm_service and services.config:
|
|
121
79
|
try:
|
|
122
|
-
|
|
80
|
+
services.llm_service = create_llm_service(services.config)
|
|
123
81
|
logger.debug(
|
|
124
|
-
f"LLM service initialized with 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
|
-
|
|
134
|
-
if mcp_manager:
|
|
91
|
+
|
|
92
|
+
if services.mcp_manager:
|
|
135
93
|
# Determine WebSocket port
|
|
136
94
|
ws_port = 60888
|
|
137
|
-
|
|
138
|
-
|
|
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,36 +108,38 @@ 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 =
|
|
160
|
-
inter_session_message_manager = InterSessionMessageManager(
|
|
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
124
|
# Setup internal registries (gobby-tasks, gobby-memory, 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=
|
|
171
|
-
message_manager=message_manager,
|
|
172
|
-
local_session_manager=session_manager,
|
|
173
|
-
metrics_manager=
|
|
174
|
-
llm_service=
|
|
175
|
-
agent_runner=
|
|
176
|
-
worktree_storage=
|
|
177
|
-
clone_storage=
|
|
178
|
-
git_manager=
|
|
128
|
+
memory_manager=services.memory_manager,
|
|
129
|
+
task_manager=services.task_manager,
|
|
130
|
+
sync_manager=services.task_sync_manager,
|
|
131
|
+
task_validator=services.task_validator,
|
|
132
|
+
message_manager=services.message_manager,
|
|
133
|
+
local_session_manager=services.session_manager,
|
|
134
|
+
metrics_manager=services.metrics_manager,
|
|
135
|
+
llm_service=services.llm_service,
|
|
136
|
+
agent_runner=services.agent_runner,
|
|
137
|
+
worktree_storage=services.worktree_storage,
|
|
138
|
+
clone_storage=services.clone_storage,
|
|
139
|
+
git_manager=services.git_manager,
|
|
179
140
|
merge_storage=merge_storage,
|
|
180
141
|
merge_resolver=merge_resolver,
|
|
181
|
-
project_id=
|
|
142
|
+
project_id=services.project_id,
|
|
182
143
|
tool_proxy_getter=tool_proxy_getter,
|
|
183
144
|
inter_session_message_manager=inter_session_message_manager,
|
|
184
145
|
)
|
|
@@ -186,47 +147,47 @@ class HTTPServer:
|
|
|
186
147
|
logger.debug(f"Internal registries initialized: {registry_count} registries")
|
|
187
148
|
|
|
188
149
|
# Initialize tool summarizer config
|
|
189
|
-
if config:
|
|
150
|
+
if services.config:
|
|
190
151
|
from gobby.tools.summarizer import init_summarizer_config
|
|
191
152
|
|
|
192
|
-
init_summarizer_config(config.tool_summarizer)
|
|
153
|
+
init_summarizer_config(services.config.tool_summarizer)
|
|
193
154
|
logger.debug("Tool summarizer config initialized")
|
|
194
155
|
|
|
195
156
|
# Create semantic search instance if db available
|
|
196
157
|
semantic_search = None
|
|
197
|
-
if mcp_db_manager:
|
|
198
|
-
semantic_search = SemanticToolSearch(db=mcp_db_manager.db)
|
|
158
|
+
if services.mcp_db_manager:
|
|
159
|
+
semantic_search = SemanticToolSearch(db=services.mcp_db_manager.db)
|
|
199
160
|
logger.debug("Semantic tool search initialized")
|
|
200
161
|
|
|
201
162
|
# Create tool filter for workflow phase restrictions
|
|
202
163
|
tool_filter = None
|
|
203
|
-
if mcp_db_manager:
|
|
204
|
-
tool_filter = ToolFilterService(db=mcp_db_manager.db)
|
|
164
|
+
if services.mcp_db_manager:
|
|
165
|
+
tool_filter = ToolFilterService(db=services.mcp_db_manager.db)
|
|
205
166
|
logger.debug("Tool filter service initialized")
|
|
206
167
|
|
|
207
168
|
# Create fallback resolver for alternative tool suggestions on error
|
|
208
169
|
fallback_resolver = None
|
|
209
|
-
if semantic_search and
|
|
170
|
+
if semantic_search and services.metrics_manager:
|
|
210
171
|
from gobby.mcp_proxy.services.fallback import ToolFallbackResolver
|
|
211
172
|
|
|
212
173
|
fallback_resolver = ToolFallbackResolver(
|
|
213
174
|
semantic_search=semantic_search,
|
|
214
|
-
metrics_manager=
|
|
175
|
+
metrics_manager=services.metrics_manager,
|
|
215
176
|
)
|
|
216
177
|
logger.debug("Fallback resolver initialized")
|
|
217
178
|
|
|
218
179
|
# Create tools handler
|
|
219
180
|
self._tools_handler = GobbyDaemonTools(
|
|
220
|
-
mcp_manager=mcp_manager,
|
|
181
|
+
mcp_manager=services.mcp_manager,
|
|
221
182
|
daemon_port=port,
|
|
222
183
|
websocket_port=ws_port,
|
|
223
184
|
start_time=self._start_time,
|
|
224
185
|
internal_manager=self._internal_manager,
|
|
225
|
-
config=config,
|
|
226
|
-
llm_service=
|
|
227
|
-
session_manager=session_manager,
|
|
228
|
-
memory_manager=memory_manager,
|
|
229
|
-
config_manager=mcp_db_manager,
|
|
186
|
+
config=services.config,
|
|
187
|
+
llm_service=services.llm_service,
|
|
188
|
+
session_manager=services.session_manager,
|
|
189
|
+
memory_manager=services.memory_manager,
|
|
190
|
+
config_manager=services.mcp_db_manager,
|
|
230
191
|
semantic_search=semantic_search,
|
|
231
192
|
tool_filter=tool_filter,
|
|
232
193
|
fallback_resolver=fallback_resolver,
|
|
@@ -240,6 +201,83 @@ class HTTPServer:
|
|
|
240
201
|
self._metrics = get_metrics_collector()
|
|
241
202
|
self._daemon: Any = None # Set externally by daemon
|
|
242
203
|
|
|
204
|
+
# Property accessors for services (delegate to container)
|
|
205
|
+
@property
|
|
206
|
+
def config(self) -> "DaemonConfig | None":
|
|
207
|
+
return self.services.config
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def session_manager(self) -> Any:
|
|
211
|
+
return self.services.session_manager
|
|
212
|
+
|
|
213
|
+
@session_manager.setter
|
|
214
|
+
def session_manager(self, value: Any) -> None:
|
|
215
|
+
self.services.session_manager = value
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def task_manager(self) -> Any:
|
|
219
|
+
return self.services.task_manager
|
|
220
|
+
|
|
221
|
+
@task_manager.setter
|
|
222
|
+
def task_manager(self, value: Any) -> None:
|
|
223
|
+
self.services.task_manager = value
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def mcp_manager(self) -> "MCPClientManager | None":
|
|
227
|
+
return self.services.mcp_manager
|
|
228
|
+
|
|
229
|
+
@mcp_manager.setter
|
|
230
|
+
def mcp_manager(self, value: "MCPClientManager | None") -> None:
|
|
231
|
+
self.services.mcp_manager = value
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def llm_service(self) -> "LLMService | None":
|
|
235
|
+
return self.services.llm_service
|
|
236
|
+
|
|
237
|
+
@llm_service.setter
|
|
238
|
+
def llm_service(self, value: "LLMService | None") -> None:
|
|
239
|
+
self.services.llm_service = value
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def memory_manager(self) -> Any:
|
|
243
|
+
return self.services.memory_manager
|
|
244
|
+
|
|
245
|
+
@memory_manager.setter
|
|
246
|
+
def memory_manager(self, value: Any) -> None:
|
|
247
|
+
self.services.memory_manager = value
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def message_manager(self) -> Any:
|
|
251
|
+
return self.services.message_manager
|
|
252
|
+
|
|
253
|
+
@message_manager.setter
|
|
254
|
+
def message_manager(self, value: Any) -> None:
|
|
255
|
+
self.services.message_manager = value
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def message_processor(self) -> Any:
|
|
259
|
+
return self.services.message_processor
|
|
260
|
+
|
|
261
|
+
@message_processor.setter
|
|
262
|
+
def message_processor(self, value: Any) -> None:
|
|
263
|
+
self.services.message_processor = value
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def metrics_manager(self) -> "ToolMetricsManager | None":
|
|
267
|
+
return self.services.metrics_manager
|
|
268
|
+
|
|
269
|
+
@metrics_manager.setter
|
|
270
|
+
def metrics_manager(self, value: "ToolMetricsManager | None") -> None:
|
|
271
|
+
self.services.metrics_manager = value
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def _mcp_db_manager(self) -> Any:
|
|
275
|
+
return self.services.mcp_db_manager
|
|
276
|
+
|
|
277
|
+
@_mcp_db_manager.setter
|
|
278
|
+
def _mcp_db_manager(self, value: Any) -> None:
|
|
279
|
+
self.services.mcp_db_manager = value
|
|
280
|
+
|
|
243
281
|
@property
|
|
244
282
|
def tool_proxy(self) -> Any:
|
|
245
283
|
"""Get the ToolProxyService instance for routing tool calls with error enrichment."""
|
|
@@ -314,30 +352,32 @@ class HTTPServer:
|
|
|
314
352
|
hook_manager_kwargs: dict[str, Any] = {
|
|
315
353
|
"daemon_host": "localhost",
|
|
316
354
|
"daemon_port": self.port,
|
|
317
|
-
"llm_service": self.llm_service,
|
|
318
|
-
"config": self.config,
|
|
355
|
+
"llm_service": self.services.llm_service,
|
|
356
|
+
"config": self.services.config,
|
|
319
357
|
"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,
|
|
358
|
+
"mcp_manager": self.services.mcp_manager,
|
|
359
|
+
"message_processor": self.services.message_processor,
|
|
360
|
+
"memory_sync_manager": self.services.memory_sync_manager,
|
|
361
|
+
"task_sync_manager": self.services.task_sync_manager,
|
|
324
362
|
}
|
|
325
|
-
if self.config:
|
|
363
|
+
if self.services.config:
|
|
326
364
|
# 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"] =
|
|
329
|
-
|
|
365
|
+
hook_manager_kwargs["log_file"] = self.services.config.logging.hook_manager
|
|
366
|
+
hook_manager_kwargs["log_max_bytes"] = (
|
|
367
|
+
self.services.config.logging.max_size_mb * 1024 * 1024
|
|
368
|
+
)
|
|
369
|
+
hook_manager_kwargs["log_backup_count"] = self.services.config.logging.backup_count
|
|
330
370
|
|
|
331
371
|
app.state.hook_manager = HookManager(**hook_manager_kwargs)
|
|
332
372
|
logger.debug("HookManager initialized in daemon")
|
|
333
373
|
|
|
334
374
|
# Wire up stop_registry to WebSocket server for stop_request handling
|
|
335
375
|
if (
|
|
336
|
-
self.websocket_server
|
|
376
|
+
self.services.websocket_server
|
|
337
377
|
and hasattr(app.state, "hook_manager")
|
|
338
378
|
and hasattr(app.state.hook_manager, "_stop_registry")
|
|
339
379
|
):
|
|
340
|
-
self.websocket_server.stop_registry = app.state.hook_manager._stop_registry
|
|
380
|
+
self.services.websocket_server.stop_registry = app.state.hook_manager._stop_registry
|
|
341
381
|
logger.debug("Stop registry connected to WebSocket server")
|
|
342
382
|
|
|
343
383
|
# Store server instance for dependency injection
|
|
@@ -496,45 +536,48 @@ class HTTPServer:
|
|
|
496
536
|
try:
|
|
497
537
|
logger.debug("Processing graceful shutdown")
|
|
498
538
|
|
|
499
|
-
#
|
|
539
|
+
# Cancel pending background tasks immediately instead of polling
|
|
500
540
|
pending_tasks_count = len(self._background_tasks)
|
|
501
541
|
if pending_tasks_count > 0:
|
|
502
542
|
logger.debug(
|
|
503
|
-
"
|
|
543
|
+
"Cancelling pending background tasks",
|
|
504
544
|
extra={"pending_tasks": pending_tasks_count},
|
|
505
545
|
)
|
|
506
546
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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},
|
|
547
|
+
# Cancel all tasks
|
|
548
|
+
for task in self._background_tasks:
|
|
549
|
+
task.cancel()
|
|
550
|
+
|
|
551
|
+
# Wait for cancellation to complete with a short timeout
|
|
552
|
+
if self._background_tasks:
|
|
553
|
+
done, pending = await asyncio.wait(
|
|
554
|
+
self._background_tasks,
|
|
555
|
+
timeout=5.0,
|
|
556
|
+
return_when=asyncio.ALL_COMPLETED,
|
|
531
557
|
)
|
|
532
558
|
|
|
559
|
+
completed_count = len(done)
|
|
560
|
+
remaining_count = len(pending)
|
|
561
|
+
|
|
562
|
+
if remaining_count > 0:
|
|
563
|
+
logger.warning(
|
|
564
|
+
"Some background tasks did not cancel in time",
|
|
565
|
+
extra={
|
|
566
|
+
"completed": completed_count,
|
|
567
|
+
"remaining": remaining_count,
|
|
568
|
+
},
|
|
569
|
+
)
|
|
570
|
+
else:
|
|
571
|
+
logger.debug(
|
|
572
|
+
"All background tasks cancelled",
|
|
573
|
+
extra={"completed": completed_count},
|
|
574
|
+
)
|
|
575
|
+
|
|
533
576
|
# Disconnect all MCP servers
|
|
534
|
-
if self.mcp_manager:
|
|
577
|
+
if self.services.mcp_manager:
|
|
535
578
|
logger.debug("Disconnecting MCP servers...")
|
|
536
579
|
try:
|
|
537
|
-
await self.mcp_manager.disconnect_all()
|
|
580
|
+
await self.services.mcp_manager.disconnect_all()
|
|
538
581
|
logger.debug("MCP servers disconnected")
|
|
539
582
|
except Exception as e:
|
|
540
583
|
logger.warning(f"Error disconnecting MCP servers: {e}")
|
|
@@ -560,31 +603,25 @@ class HTTPServer:
|
|
|
560
603
|
|
|
561
604
|
|
|
562
605
|
async def create_server(
|
|
606
|
+
services: "ServiceContainer",
|
|
563
607
|
port: int = 60887,
|
|
564
608
|
test_mode: bool = False,
|
|
565
|
-
mcp_manager: Any | None = None,
|
|
566
|
-
config: Any | None = None,
|
|
567
|
-
session_manager: LocalSessionManager | None = None,
|
|
568
609
|
) -> HTTPServer:
|
|
569
610
|
"""
|
|
570
611
|
Create HTTP server instance.
|
|
571
612
|
|
|
572
613
|
Args:
|
|
614
|
+
services: ServiceContainer holding dependencies
|
|
573
615
|
port: Port to listen on
|
|
574
616
|
test_mode: Enable test mode
|
|
575
|
-
mcp_manager: MCP client manager
|
|
576
|
-
config: Daemon configuration
|
|
577
|
-
session_manager: Local session manager
|
|
578
617
|
|
|
579
618
|
Returns:
|
|
580
619
|
Configured HTTPServer instance
|
|
581
620
|
"""
|
|
582
621
|
return HTTPServer(
|
|
622
|
+
services=services,
|
|
583
623
|
port=port,
|
|
584
624
|
test_mode=test_mode,
|
|
585
|
-
mcp_manager=mcp_manager,
|
|
586
|
-
config=config,
|
|
587
|
-
session_manager=session_manager,
|
|
588
625
|
)
|
|
589
626
|
|
|
590
627
|
|