gobby 0.2.5__py3-none-any.whl → 0.2.7__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/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -7,13 +7,30 @@ import logging
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any, Literal
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
-
from gobby.config.
|
|
10
|
+
from gobby.config.features import RecommendToolsConfig
|
|
11
|
+
from gobby.prompts import PromptLoader
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger("gobby.mcp.server")
|
|
13
14
|
|
|
14
15
|
# Search mode type
|
|
15
16
|
SearchMode = Literal["llm", "semantic", "hybrid"]
|
|
16
17
|
|
|
18
|
+
DEFAULT_HYBRID_RERANK_PROMPT = """Re-rank the following tools for the task: "{task_description}"
|
|
19
|
+
|
|
20
|
+
Candidates:
|
|
21
|
+
{candidate_list}
|
|
22
|
+
|
|
23
|
+
Select the best {top_k} tools. Return JSON:
|
|
24
|
+
{{"recommendations": [{{"server": "...", "tool": "...", "reason": "..."}}]}}"""
|
|
25
|
+
|
|
26
|
+
DEFAULT_LLM_PROMPT = """Recommend tools for the task: "{task_description}"
|
|
27
|
+
|
|
28
|
+
Available Servers:
|
|
29
|
+
{available_servers}
|
|
30
|
+
|
|
31
|
+
Return JSON:
|
|
32
|
+
{{"recommendations": [{{"server": "...", "tool": "...", "reason": "..."}}]}}"""
|
|
33
|
+
|
|
17
34
|
|
|
18
35
|
class RecommendationService:
|
|
19
36
|
"""Service for recommending tools."""
|
|
@@ -31,12 +48,17 @@ class RecommendationService:
|
|
|
31
48
|
self._semantic_search = semantic_search
|
|
32
49
|
self._project_id = project_id
|
|
33
50
|
self._config = config
|
|
51
|
+
self._loader = PromptLoader()
|
|
52
|
+
self._loader.register_fallback(
|
|
53
|
+
"features/recommend_hybrid", lambda: DEFAULT_HYBRID_RERANK_PROMPT
|
|
54
|
+
)
|
|
55
|
+
self._loader.register_fallback("features/recommend_llm", lambda: DEFAULT_LLM_PROMPT)
|
|
34
56
|
|
|
35
57
|
def _get_config(self) -> RecommendToolsConfig:
|
|
36
58
|
"""Get config with fallback to defaults."""
|
|
37
59
|
if self._config is not None:
|
|
38
60
|
return self._config
|
|
39
|
-
from gobby.config.
|
|
61
|
+
from gobby.config.features import RecommendToolsConfig
|
|
40
62
|
|
|
41
63
|
return RecommendToolsConfig()
|
|
42
64
|
|
|
@@ -153,11 +175,16 @@ class RecommendationService:
|
|
|
153
175
|
for c in candidates
|
|
154
176
|
)
|
|
155
177
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
178
|
+
prompt_path = config.hybrid_rerank_prompt_path or "features/recommend_hybrid"
|
|
179
|
+
context = {
|
|
180
|
+
"task_description": task_description,
|
|
181
|
+
"candidate_list": candidate_list,
|
|
182
|
+
"top_k": top_k,
|
|
183
|
+
}
|
|
184
|
+
try:
|
|
185
|
+
prompt = self._loader.render(prompt_path, context)
|
|
186
|
+
except Exception:
|
|
187
|
+
prompt = DEFAULT_HYBRID_RERANK_PROMPT.format(**context)
|
|
161
188
|
|
|
162
189
|
provider = self._llm_service.get_default_provider()
|
|
163
190
|
response = await provider.generate_text(prompt)
|
|
@@ -191,10 +218,15 @@ class RecommendationService:
|
|
|
191
218
|
config = self._get_config()
|
|
192
219
|
available_servers = self._mcp_manager.get_available_servers()
|
|
193
220
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
221
|
+
prompt_path = config.llm_prompt_path or "features/recommend_llm"
|
|
222
|
+
context = {
|
|
223
|
+
"task_description": task_description,
|
|
224
|
+
"available_servers": ", ".join(available_servers),
|
|
225
|
+
}
|
|
226
|
+
try:
|
|
227
|
+
prompt = self._loader.render(prompt_path, context)
|
|
228
|
+
except Exception:
|
|
229
|
+
prompt = DEFAULT_LLM_PROMPT.format(**context)
|
|
198
230
|
|
|
199
231
|
provider = self._llm_service.get_default_provider()
|
|
200
232
|
response = await provider.generate_text(prompt)
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from gobby.mcp_proxy.manager import MCPClientManager
|
|
7
|
-
from gobby.mcp_proxy.models import MCPError
|
|
7
|
+
from gobby.mcp_proxy.models import MCPError, ToolProxyErrorCode
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from gobby.mcp_proxy.services.fallback import ToolFallbackResolver
|
|
@@ -74,6 +74,70 @@ class ToolProxyService:
|
|
|
74
74
|
|
|
75
75
|
return errors
|
|
76
76
|
|
|
77
|
+
def _is_argument_error(self, error_message: str) -> bool:
|
|
78
|
+
"""Detect if error message suggests invalid arguments.
|
|
79
|
+
|
|
80
|
+
Used to determine whether to include tool schema in error response
|
|
81
|
+
to help the caller self-correct.
|
|
82
|
+
"""
|
|
83
|
+
indicators = [
|
|
84
|
+
"parameter",
|
|
85
|
+
"argument",
|
|
86
|
+
"required",
|
|
87
|
+
"missing",
|
|
88
|
+
"invalid",
|
|
89
|
+
"unknown",
|
|
90
|
+
"expected",
|
|
91
|
+
"type error",
|
|
92
|
+
"validation",
|
|
93
|
+
"schema",
|
|
94
|
+
"property",
|
|
95
|
+
"field",
|
|
96
|
+
"400",
|
|
97
|
+
"422",
|
|
98
|
+
"-32602", # JSON-RPC invalid params error code
|
|
99
|
+
]
|
|
100
|
+
error_lower = error_message.lower()
|
|
101
|
+
return any(indicator in error_lower for indicator in indicators)
|
|
102
|
+
|
|
103
|
+
def _classify_error(self, error_message: str, exception: Exception) -> str:
|
|
104
|
+
"""Classify an error into a structured error code.
|
|
105
|
+
|
|
106
|
+
Used to provide structured error codes that consumers can rely on
|
|
107
|
+
instead of fragile string matching.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
error_message: The error message string
|
|
111
|
+
exception: The original exception
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
ToolProxyErrorCode value as string
|
|
115
|
+
"""
|
|
116
|
+
error_lower = error_message.lower()
|
|
117
|
+
|
|
118
|
+
# Check for server not found/configured errors
|
|
119
|
+
if "server" in error_lower:
|
|
120
|
+
if "not found" in error_lower:
|
|
121
|
+
return ToolProxyErrorCode.SERVER_NOT_FOUND.value
|
|
122
|
+
if "not configured" in error_lower:
|
|
123
|
+
return ToolProxyErrorCode.SERVER_NOT_CONFIGURED.value
|
|
124
|
+
|
|
125
|
+
# Check for tool not found
|
|
126
|
+
if "tool" in error_lower and "not found" in error_lower:
|
|
127
|
+
return ToolProxyErrorCode.TOOL_NOT_FOUND.value
|
|
128
|
+
|
|
129
|
+
# Check for argument/validation errors
|
|
130
|
+
if self._is_argument_error(error_message):
|
|
131
|
+
return ToolProxyErrorCode.INVALID_ARGUMENTS.value
|
|
132
|
+
|
|
133
|
+
# Check for connection errors
|
|
134
|
+
connection_indicators = ["connection", "timeout", "refused", "unreachable", "circuit"]
|
|
135
|
+
if any(ind in error_lower for ind in connection_indicators):
|
|
136
|
+
return ToolProxyErrorCode.CONNECTION_ERROR.value
|
|
137
|
+
|
|
138
|
+
# Default to execution error
|
|
139
|
+
return ToolProxyErrorCode.EXECUTION_ERROR.value
|
|
140
|
+
|
|
77
141
|
async def list_tools(
|
|
78
142
|
self,
|
|
79
143
|
server_name: str,
|
|
@@ -193,10 +257,26 @@ class ToolProxyService:
|
|
|
193
257
|
response: dict[str, Any] = {
|
|
194
258
|
"success": False,
|
|
195
259
|
"error": error_message,
|
|
260
|
+
"error_code": self._classify_error(error_message, e),
|
|
196
261
|
"server_name": server_name,
|
|
197
262
|
"tool_name": tool_name,
|
|
198
263
|
}
|
|
199
264
|
|
|
265
|
+
# Enrich with schema if error looks like an argument validation error
|
|
266
|
+
if self._is_argument_error(error_message):
|
|
267
|
+
try:
|
|
268
|
+
schema_result = await self.get_tool_schema(server_name, tool_name)
|
|
269
|
+
if schema_result.get("success"):
|
|
270
|
+
input_schema = schema_result.get("tool", {}).get("inputSchema", {})
|
|
271
|
+
if input_schema:
|
|
272
|
+
response["hint"] = (
|
|
273
|
+
"This appears to be an argument error. "
|
|
274
|
+
"Schema provided for self-correction."
|
|
275
|
+
)
|
|
276
|
+
response["schema"] = input_schema
|
|
277
|
+
except Exception as schema_error:
|
|
278
|
+
logger.debug(f"Could not fetch schema for error enrichment: {schema_error}")
|
|
279
|
+
|
|
200
280
|
# Get fallback suggestions if resolver is available
|
|
201
281
|
if self._fallback_resolver:
|
|
202
282
|
try:
|
gobby/mcp_proxy/stdio.py
CHANGED
|
@@ -22,6 +22,7 @@ from gobby.mcp_proxy.daemon_control import (
|
|
|
22
22
|
start_daemon_process,
|
|
23
23
|
stop_daemon_process,
|
|
24
24
|
)
|
|
25
|
+
from gobby.mcp_proxy.instructions import build_gobby_instructions
|
|
25
26
|
from gobby.mcp_proxy.registries import setup_internal_registries
|
|
26
27
|
|
|
27
28
|
__all__ = [
|
|
@@ -264,7 +265,7 @@ def create_stdio_mcp_server() -> FastMCP:
|
|
|
264
265
|
_ = setup_internal_registries(config, session_manager, memory_manager)
|
|
265
266
|
|
|
266
267
|
# Initialize MCP server and daemon proxy
|
|
267
|
-
mcp = FastMCP("gobby")
|
|
268
|
+
mcp = FastMCP("gobby", instructions=build_gobby_instructions())
|
|
268
269
|
proxy = DaemonProxy(config.daemon_port)
|
|
269
270
|
|
|
270
271
|
register_proxy_tools(mcp, proxy)
|
|
@@ -7,7 +7,6 @@ Provides factory functions for creating tool registries.
|
|
|
7
7
|
# Main task registry (facade that merges all task-related registries)
|
|
8
8
|
# Extracted task module registries (for direct use or testing)
|
|
9
9
|
from gobby.mcp_proxy.tools.task_dependencies import create_dependency_registry
|
|
10
|
-
from gobby.mcp_proxy.tools.task_expansion import create_expansion_registry
|
|
11
10
|
from gobby.mcp_proxy.tools.task_github import create_github_sync_registry
|
|
12
11
|
from gobby.mcp_proxy.tools.task_readiness import create_readiness_registry
|
|
13
12
|
from gobby.mcp_proxy.tools.task_sync import create_sync_registry
|
|
@@ -19,7 +18,6 @@ __all__ = [
|
|
|
19
18
|
"create_task_registry",
|
|
20
19
|
# Extracted registries
|
|
21
20
|
"create_dependency_registry",
|
|
22
|
-
"create_expansion_registry",
|
|
23
21
|
"create_github_sync_registry",
|
|
24
22
|
"create_readiness_registry",
|
|
25
23
|
"create_sync_registry",
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inter-agent messaging tools for the gobby-agents MCP server.
|
|
3
|
+
|
|
4
|
+
Provides messaging capabilities between parent and child sessions:
|
|
5
|
+
- send_to_parent: Child sends message to its parent session
|
|
6
|
+
- send_to_child: Parent sends message to a specific child
|
|
7
|
+
- poll_messages: Check for incoming messages
|
|
8
|
+
- mark_message_read: Mark a message as read
|
|
9
|
+
- broadcast_to_children: Send message to all running children
|
|
10
|
+
|
|
11
|
+
These tools resolve session relationships from RunningAgentRegistry.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from gobby.agents.registry import RunningAgentRegistry
|
|
21
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
22
|
+
from gobby.storage.inter_session_messages import InterSessionMessageManager
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def add_messaging_tools(
|
|
28
|
+
registry: InternalToolRegistry,
|
|
29
|
+
message_manager: InterSessionMessageManager,
|
|
30
|
+
agent_registry: RunningAgentRegistry,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Add inter-agent messaging tools to an existing registry.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
registry: The InternalToolRegistry to add tools to (typically gobby-agents)
|
|
37
|
+
message_manager: InterSessionMessageManager for persisting messages
|
|
38
|
+
agent_registry: RunningAgentRegistry for resolving parent/child relationships
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@registry.tool(
|
|
42
|
+
name="send_to_parent",
|
|
43
|
+
description="Send a message from a child session to its parent session.",
|
|
44
|
+
)
|
|
45
|
+
async def send_to_parent(
|
|
46
|
+
session_id: str,
|
|
47
|
+
content: str,
|
|
48
|
+
priority: str = "normal",
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Send a message to the parent session.
|
|
52
|
+
|
|
53
|
+
Use this when a child agent needs to communicate status, results,
|
|
54
|
+
or requests back to its parent session.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
session_id: The current (child) session ID
|
|
58
|
+
content: Message content to send
|
|
59
|
+
priority: Message priority ("normal" or "urgent")
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict with success status and message details
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
# Find the running agent to get parent relationship
|
|
66
|
+
agent = agent_registry.get_by_session(session_id)
|
|
67
|
+
if not agent:
|
|
68
|
+
return {
|
|
69
|
+
"success": False,
|
|
70
|
+
"error": f"Session {session_id} not found in running agent registry",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
parent_session_id = agent.parent_session_id
|
|
74
|
+
if not parent_session_id:
|
|
75
|
+
return {
|
|
76
|
+
"success": False,
|
|
77
|
+
"error": "No parent session found for this agent",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Create the message
|
|
81
|
+
msg = message_manager.create_message(
|
|
82
|
+
from_session=session_id,
|
|
83
|
+
to_session=parent_session_id,
|
|
84
|
+
content=content,
|
|
85
|
+
priority=priority,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
logger.info(f"Message sent from {session_id} to parent {parent_session_id}: {msg.id}")
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"success": True,
|
|
92
|
+
"message": msg.to_dict(),
|
|
93
|
+
"parent_session_id": parent_session_id,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Failed to send message to parent: {e}")
|
|
98
|
+
return {
|
|
99
|
+
"success": False,
|
|
100
|
+
"error": str(e),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@registry.tool(
|
|
104
|
+
name="send_to_child",
|
|
105
|
+
description="Send a message from a parent session to a specific child session.",
|
|
106
|
+
)
|
|
107
|
+
async def send_to_child(
|
|
108
|
+
parent_session_id: str,
|
|
109
|
+
child_session_id: str,
|
|
110
|
+
content: str,
|
|
111
|
+
priority: str = "normal",
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
"""
|
|
114
|
+
Send a message to a child session.
|
|
115
|
+
|
|
116
|
+
Use this when a parent agent needs to communicate instructions,
|
|
117
|
+
updates, or coordination messages to a spawned child.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
parent_session_id: The parent session ID (sender)
|
|
121
|
+
child_session_id: The child session ID (recipient)
|
|
122
|
+
content: Message content to send
|
|
123
|
+
priority: Message priority ("normal" or "urgent")
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dict with success status and message details
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
# Verify the child exists and belongs to this parent
|
|
130
|
+
child_agent = agent_registry.get_by_session(child_session_id)
|
|
131
|
+
if not child_agent:
|
|
132
|
+
return {
|
|
133
|
+
"success": False,
|
|
134
|
+
"error": f"Child session {child_session_id} not found in running agent registry",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if child_agent.parent_session_id != parent_session_id:
|
|
138
|
+
return {
|
|
139
|
+
"success": False,
|
|
140
|
+
"error": (
|
|
141
|
+
f"Session {child_session_id} is not a child of {parent_session_id}. "
|
|
142
|
+
f"Actual parent: {child_agent.parent_session_id}"
|
|
143
|
+
),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Create the message
|
|
147
|
+
msg = message_manager.create_message(
|
|
148
|
+
from_session=parent_session_id,
|
|
149
|
+
to_session=child_session_id,
|
|
150
|
+
content=content,
|
|
151
|
+
priority=priority,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
logger.info(
|
|
155
|
+
f"Message sent from {parent_session_id} to child {child_session_id}: {msg.id}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"success": True,
|
|
160
|
+
"message": msg.to_dict(),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"Failed to send message to child: {e}")
|
|
165
|
+
return {
|
|
166
|
+
"success": False,
|
|
167
|
+
"error": str(e),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@registry.tool(
|
|
171
|
+
name="poll_messages",
|
|
172
|
+
description="Poll for messages sent to this session.",
|
|
173
|
+
)
|
|
174
|
+
async def poll_messages(
|
|
175
|
+
session_id: str,
|
|
176
|
+
unread_only: bool = True,
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""
|
|
179
|
+
Poll for incoming messages.
|
|
180
|
+
|
|
181
|
+
Check for messages sent to this session from parent or child sessions.
|
|
182
|
+
By default, returns only unread messages.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
session_id: The session ID to check messages for
|
|
186
|
+
unread_only: If True, only return unread messages (default: True)
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Dict with success status and list of messages
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
messages = message_manager.get_messages(
|
|
193
|
+
to_session=session_id,
|
|
194
|
+
unread_only=unread_only,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"success": True,
|
|
199
|
+
"messages": [msg.to_dict() for msg in messages],
|
|
200
|
+
"count": len(messages),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.error(f"Failed to poll messages: {e}")
|
|
205
|
+
return {
|
|
206
|
+
"success": False,
|
|
207
|
+
"error": str(e),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@registry.tool(
|
|
211
|
+
name="mark_message_read",
|
|
212
|
+
description="Mark a message as read.",
|
|
213
|
+
)
|
|
214
|
+
async def mark_message_read(
|
|
215
|
+
message_id: str,
|
|
216
|
+
) -> dict[str, Any]:
|
|
217
|
+
"""
|
|
218
|
+
Mark a message as read.
|
|
219
|
+
|
|
220
|
+
After processing a message, mark it as read so it won't appear
|
|
221
|
+
in subsequent poll_messages calls with unread_only=True.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
message_id: The message ID to mark as read
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Dict with success status and updated message
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
msg = message_manager.mark_read(message_id)
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
"success": True,
|
|
234
|
+
"message": msg.to_dict(),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
except ValueError:
|
|
238
|
+
return {
|
|
239
|
+
"success": False,
|
|
240
|
+
"error": f"Message not found: {message_id}",
|
|
241
|
+
}
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.error(f"Failed to mark message as read: {e}")
|
|
244
|
+
return {
|
|
245
|
+
"success": False,
|
|
246
|
+
"error": str(e),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@registry.tool(
|
|
250
|
+
name="broadcast_to_children",
|
|
251
|
+
description="Broadcast a message to all running child sessions.",
|
|
252
|
+
)
|
|
253
|
+
async def broadcast_to_children(
|
|
254
|
+
parent_session_id: str,
|
|
255
|
+
content: str,
|
|
256
|
+
priority: str = "normal",
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
"""
|
|
259
|
+
Broadcast a message to all running children.
|
|
260
|
+
|
|
261
|
+
Send the same message to all child sessions spawned by this parent.
|
|
262
|
+
Useful for coordination or shutdown signals.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
parent_session_id: The parent session ID
|
|
266
|
+
content: Message content to broadcast
|
|
267
|
+
priority: Message priority ("normal" or "urgent")
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Dict with success status and count of messages sent
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
children = agent_registry.list_by_parent(parent_session_id)
|
|
274
|
+
|
|
275
|
+
if not children:
|
|
276
|
+
return {
|
|
277
|
+
"success": True,
|
|
278
|
+
"sent_count": 0,
|
|
279
|
+
"message": "No running children found",
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
sent_count = 0
|
|
283
|
+
errors = []
|
|
284
|
+
|
|
285
|
+
for child in children:
|
|
286
|
+
try:
|
|
287
|
+
message_manager.create_message(
|
|
288
|
+
from_session=parent_session_id,
|
|
289
|
+
to_session=child.session_id,
|
|
290
|
+
content=content,
|
|
291
|
+
priority=priority,
|
|
292
|
+
)
|
|
293
|
+
sent_count += 1
|
|
294
|
+
except Exception as e:
|
|
295
|
+
errors.append(f"{child.session_id}: {e}")
|
|
296
|
+
|
|
297
|
+
result: dict[str, Any] = {
|
|
298
|
+
"success": True,
|
|
299
|
+
"sent_count": sent_count,
|
|
300
|
+
"total_children": len(children),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if errors:
|
|
304
|
+
result["errors"] = errors
|
|
305
|
+
|
|
306
|
+
logger.info(
|
|
307
|
+
f"Broadcast from {parent_session_id} sent to {sent_count}/{len(children)} children"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return result
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error(f"Failed to broadcast to children: {e}")
|
|
314
|
+
return {
|
|
315
|
+
"success": False,
|
|
316
|
+
"error": str(e),
|
|
317
|
+
}
|