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.
Files changed (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {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.app import RecommendToolsConfig
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.app import RecommendToolsConfig
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
- prompt = config.hybrid_rerank_prompt.format(
157
- task_description=task_description,
158
- candidate_list=candidate_list,
159
- top_k=top_k,
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
- prompt = config.llm_prompt.format(
195
- task_description=task_description,
196
- available_servers=", ".join(available_servers),
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
+ }