gobby 0.2.8__py3-none-any.whl → 0.2.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +5 -28
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +64 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/utils.py +5 -17
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +23 -2
- gobby/config/tasks.py +4 -0
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +92 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +487 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +27 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +594 -43
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +66 -5
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +273 -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 +332 -0
- gobby/mcp_proxy/tools/workflows/_query.py +226 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/context.py +5 -5
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +131 -16
- gobby/servers/http.py +193 -150
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +56 -0
- gobby/servers/routes/mcp/endpoints/execution.py +33 -32
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +89 -3
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +118 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +48 -8
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- 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 +80 -0
- gobby/workflows/context_actions.py +265 -27
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +26 -4
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +59 -27
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/llm/service.py
CHANGED
|
@@ -6,7 +6,9 @@ Gemini, LiteLLM) based on the multi-provider config structure with feature-speci
|
|
|
6
6
|
provider routing.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import asyncio
|
|
9
10
|
import logging
|
|
11
|
+
from collections.abc import AsyncIterator
|
|
10
12
|
from typing import TYPE_CHECKING, Any
|
|
11
13
|
|
|
12
14
|
if TYPE_CHECKING:
|
|
@@ -14,6 +16,7 @@ if TYPE_CHECKING:
|
|
|
14
16
|
DaemonConfig,
|
|
15
17
|
)
|
|
16
18
|
from gobby.llm.base import LLMProvider
|
|
19
|
+
from gobby.llm.claude import ChatEvent
|
|
17
20
|
|
|
18
21
|
logger = logging.getLogger(__name__)
|
|
19
22
|
|
|
@@ -234,3 +237,149 @@ class LLMService:
|
|
|
234
237
|
enabled = self.enabled_providers
|
|
235
238
|
initialized = self.initialized_providers
|
|
236
239
|
return f"LLMService(enabled={enabled}, initialized={initialized})"
|
|
240
|
+
|
|
241
|
+
async def stream_chat(
|
|
242
|
+
self,
|
|
243
|
+
messages: list[dict[str, str]],
|
|
244
|
+
provider_name: str | None = None,
|
|
245
|
+
model: str | None = None,
|
|
246
|
+
) -> AsyncIterator[str]:
|
|
247
|
+
"""
|
|
248
|
+
Stream a chat response from the LLM.
|
|
249
|
+
|
|
250
|
+
Takes messages in OpenAI-style format and yields response chunks.
|
|
251
|
+
Currently simulates streaming by chunking the full response.
|
|
252
|
+
Real streaming support can be added per-provider later.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
messages: List of message dicts with 'role' and 'content' keys
|
|
256
|
+
provider_name: Optional provider to use (defaults to default provider)
|
|
257
|
+
model: Optional model override
|
|
258
|
+
|
|
259
|
+
Yields:
|
|
260
|
+
String chunks of the response
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
messages = [
|
|
264
|
+
{"role": "system", "content": "You are helpful."},
|
|
265
|
+
{"role": "user", "content": "Hello!"}
|
|
266
|
+
]
|
|
267
|
+
async for chunk in service.stream_chat(messages):
|
|
268
|
+
print(chunk, end="", flush=True)
|
|
269
|
+
"""
|
|
270
|
+
# Get provider
|
|
271
|
+
if provider_name:
|
|
272
|
+
provider = self.get_provider(provider_name)
|
|
273
|
+
else:
|
|
274
|
+
provider = self.get_default_provider()
|
|
275
|
+
|
|
276
|
+
# Build prompt from messages
|
|
277
|
+
system_prompt = None
|
|
278
|
+
user_messages = []
|
|
279
|
+
|
|
280
|
+
for msg in messages:
|
|
281
|
+
role = msg.get("role", "user")
|
|
282
|
+
content = msg.get("content", "")
|
|
283
|
+
|
|
284
|
+
if role == "system":
|
|
285
|
+
system_prompt = content
|
|
286
|
+
else:
|
|
287
|
+
prefix = "User: " if role == "user" else "Assistant: "
|
|
288
|
+
user_messages.append(f"{prefix}{content}")
|
|
289
|
+
|
|
290
|
+
prompt = "\n\n".join(user_messages)
|
|
291
|
+
if user_messages:
|
|
292
|
+
prompt += "\n\nAssistant:"
|
|
293
|
+
|
|
294
|
+
# Generate full response
|
|
295
|
+
response = await provider.generate_text(
|
|
296
|
+
prompt=prompt,
|
|
297
|
+
system_prompt=system_prompt,
|
|
298
|
+
model=model,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Simulate streaming by yielding words with small delays
|
|
302
|
+
# This provides a better UX while we add real streaming later
|
|
303
|
+
words = response.split(" ")
|
|
304
|
+
for i, word in enumerate(words):
|
|
305
|
+
if i > 0:
|
|
306
|
+
yield " "
|
|
307
|
+
yield word
|
|
308
|
+
# Small delay to simulate streaming (5-15ms per word)
|
|
309
|
+
await asyncio.sleep(0.008)
|
|
310
|
+
|
|
311
|
+
async def stream_chat_with_tools(
|
|
312
|
+
self,
|
|
313
|
+
messages: list[dict[str, str]],
|
|
314
|
+
allowed_tools: list[str],
|
|
315
|
+
model: str | None = None,
|
|
316
|
+
max_turns: int = 10,
|
|
317
|
+
) -> AsyncIterator["ChatEvent"]:
|
|
318
|
+
"""
|
|
319
|
+
Stream a chat response with MCP tool support.
|
|
320
|
+
|
|
321
|
+
Takes messages in OpenAI-style format and streams response events
|
|
322
|
+
including text chunks and tool call/result events.
|
|
323
|
+
|
|
324
|
+
This method uses the Claude provider's stream_with_mcp_tools(),
|
|
325
|
+
which requires subscription mode (Claude Agent SDK).
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
messages: List of message dicts with 'role' and 'content' keys
|
|
329
|
+
allowed_tools: List of allowed MCP tool patterns.
|
|
330
|
+
Tools should be in format "mcp__{server}__{tool}" or patterns
|
|
331
|
+
like "mcp__gobby-tasks__*" for all tools from a server.
|
|
332
|
+
model: Optional model override
|
|
333
|
+
max_turns: Maximum number of agentic turns (default: 10)
|
|
334
|
+
|
|
335
|
+
Yields:
|
|
336
|
+
ChatEvent: One of TextChunk, ToolCallEvent, ToolResultEvent, or DoneEvent.
|
|
337
|
+
|
|
338
|
+
Example:
|
|
339
|
+
>>> allowed_tools = ["mcp__gobby-tasks__*", "mcp__gobby-memory__*"]
|
|
340
|
+
>>> async for event in service.stream_chat_with_tools(messages, allowed_tools):
|
|
341
|
+
... if isinstance(event, TextChunk):
|
|
342
|
+
... print(event.content, end="")
|
|
343
|
+
"""
|
|
344
|
+
from gobby.llm.claude import ClaudeLLMProvider, DoneEvent, TextChunk
|
|
345
|
+
|
|
346
|
+
# Get Claude provider (required for MCP tools)
|
|
347
|
+
try:
|
|
348
|
+
provider = self.get_provider("claude")
|
|
349
|
+
except ValueError:
|
|
350
|
+
yield TextChunk(content="Claude provider not configured. MCP tools require Claude.")
|
|
351
|
+
yield DoneEvent(tool_calls_count=0)
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
if not isinstance(provider, ClaudeLLMProvider):
|
|
355
|
+
yield TextChunk(content="MCP tools require Claude provider.")
|
|
356
|
+
yield DoneEvent(tool_calls_count=0)
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
# Build system prompt and user prompt from messages
|
|
360
|
+
system_prompt = None
|
|
361
|
+
user_messages = []
|
|
362
|
+
|
|
363
|
+
for msg in messages:
|
|
364
|
+
role = msg.get("role", "user")
|
|
365
|
+
content = msg.get("content", "")
|
|
366
|
+
|
|
367
|
+
if role == "system":
|
|
368
|
+
system_prompt = content
|
|
369
|
+
else:
|
|
370
|
+
prefix = "User: " if role == "user" else "Assistant: "
|
|
371
|
+
user_messages.append(f"{prefix}{content}")
|
|
372
|
+
|
|
373
|
+
prompt = "\n\n".join(user_messages)
|
|
374
|
+
if user_messages:
|
|
375
|
+
prompt += "\n\nAssistant:"
|
|
376
|
+
|
|
377
|
+
# Stream with MCP tools
|
|
378
|
+
async for event in provider.stream_with_mcp_tools(
|
|
379
|
+
prompt=prompt,
|
|
380
|
+
allowed_tools=allowed_tools,
|
|
381
|
+
system_prompt=system_prompt,
|
|
382
|
+
model=model,
|
|
383
|
+
max_turns=max_turns,
|
|
384
|
+
):
|
|
385
|
+
yield event
|
gobby/mcp_proxy/importer.py
CHANGED
|
@@ -5,7 +5,6 @@ import re
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
from gobby.config.app import DaemonConfig
|
|
8
|
-
from gobby.config.features import DEFAULT_IMPORT_MCP_SERVER_PROMPT
|
|
9
8
|
from gobby.prompts import PromptLoader
|
|
10
9
|
from gobby.storage.database import DatabaseProtocol
|
|
11
10
|
from gobby.storage.mcp import LocalMCPManager
|
|
@@ -20,21 +19,6 @@ logger = logging.getLogger(__name__)
|
|
|
20
19
|
# Pattern to detect placeholder secrets like <YOUR_API_KEY>
|
|
21
20
|
SECRET_PLACEHOLDER_PATTERN = re.compile(r"<YOUR_[A-Z0-9_]+>")
|
|
22
21
|
|
|
23
|
-
DEFAULT_GITHUB_FETCH_PROMPT = """Fetch the README from this GitHub repository and extract MCP server configuration:
|
|
24
|
-
|
|
25
|
-
{github_url}
|
|
26
|
-
|
|
27
|
-
If the URL doesn't point directly to a README, try to find and fetch the README.md file.
|
|
28
|
-
|
|
29
|
-
After reading the documentation, extract the MCP server configuration as a JSON object."""
|
|
30
|
-
|
|
31
|
-
DEFAULT_SEARCH_FETCH_PROMPT = """Search for MCP server: {search_query}
|
|
32
|
-
|
|
33
|
-
Find the official documentation or GitHub repository for this MCP server.
|
|
34
|
-
Then fetch and read the README or installation docs.
|
|
35
|
-
|
|
36
|
-
After reading the documentation, extract the MCP server configuration as a JSON object."""
|
|
37
|
-
|
|
38
22
|
|
|
39
23
|
class MCPServerImporter:
|
|
40
24
|
"""Handles importing MCP servers from various sources."""
|
|
@@ -73,11 +57,6 @@ class MCPServerImporter:
|
|
|
73
57
|
|
|
74
58
|
self._loader = PromptLoader(project_dir=Path(project_path) if project_path else None)
|
|
75
59
|
|
|
76
|
-
# Register fallbacks
|
|
77
|
-
self._loader.register_fallback("import/github_fetch", lambda: DEFAULT_GITHUB_FETCH_PROMPT)
|
|
78
|
-
self._loader.register_fallback("import/search_fetch", lambda: DEFAULT_SEARCH_FETCH_PROMPT)
|
|
79
|
-
self._loader.register_fallback("import/system", lambda: DEFAULT_IMPORT_MCP_SERVER_PROMPT)
|
|
80
|
-
|
|
81
60
|
async def import_from_project(
|
|
82
61
|
self,
|
|
83
62
|
source_project: str,
|
|
@@ -204,19 +183,11 @@ class MCPServerImporter:
|
|
|
204
183
|
|
|
205
184
|
# Build prompt to fetch and extract config
|
|
206
185
|
prompt_path = self.import_config.github_fetch_prompt_path or "import/github_fetch"
|
|
207
|
-
|
|
208
|
-
prompt = self._loader.render(prompt_path, {"github_url": github_url})
|
|
209
|
-
except Exception as e:
|
|
210
|
-
logger.warning(f"Failed to load Github fetch prompt: {e}")
|
|
211
|
-
prompt = DEFAULT_GITHUB_FETCH_PROMPT.format(github_url=github_url)
|
|
186
|
+
prompt = self._loader.render(prompt_path, {"github_url": github_url})
|
|
212
187
|
|
|
213
188
|
# Get system prompt
|
|
214
189
|
sys_prompt_path = self.import_config.prompt_path or "import/system"
|
|
215
|
-
|
|
216
|
-
system_prompt = self._loader.render(sys_prompt_path, {})
|
|
217
|
-
except Exception as e:
|
|
218
|
-
logger.warning(f"Failed to load import system prompt: {e}")
|
|
219
|
-
system_prompt = DEFAULT_IMPORT_MCP_SERVER_PROMPT
|
|
190
|
+
system_prompt = self._loader.render(sys_prompt_path, {})
|
|
220
191
|
|
|
221
192
|
options = ClaudeAgentOptions(
|
|
222
193
|
system_prompt=system_prompt,
|
|
@@ -268,19 +239,11 @@ class MCPServerImporter:
|
|
|
268
239
|
|
|
269
240
|
# Build prompt to search and extract config
|
|
270
241
|
prompt_path = self.import_config.search_fetch_prompt_path or "import/search_fetch"
|
|
271
|
-
|
|
272
|
-
prompt = self._loader.render(prompt_path, {"search_query": search_query})
|
|
273
|
-
except Exception as e:
|
|
274
|
-
logger.warning(f"Failed to load search fetch prompt: {e}")
|
|
275
|
-
prompt = DEFAULT_SEARCH_FETCH_PROMPT.format(search_query=search_query)
|
|
242
|
+
prompt = self._loader.render(prompt_path, {"search_query": search_query})
|
|
276
243
|
|
|
277
244
|
# Get system prompt
|
|
278
245
|
sys_prompt_path = self.import_config.prompt_path or "import/system"
|
|
279
|
-
|
|
280
|
-
system_prompt = self._loader.render(sys_prompt_path, {})
|
|
281
|
-
except Exception as e:
|
|
282
|
-
logger.warning(f"Failed to load import system prompt: {e}")
|
|
283
|
-
system_prompt = DEFAULT_IMPORT_MCP_SERVER_PROMPT
|
|
246
|
+
system_prompt = self._loader.render(sys_prompt_path, {})
|
|
284
247
|
|
|
285
248
|
options = ClaudeAgentOptions(
|
|
286
249
|
system_prompt=system_prompt,
|
gobby/mcp_proxy/instructions.py
CHANGED
|
@@ -6,33 +6,16 @@ These instructions are injected into the MCP server via FastMCP's `instructions`
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def build_gobby_instructions() -> str:
|
|
9
|
-
"""Build
|
|
9
|
+
"""Build compact instructions for Gobby MCP server.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
The instructions cover:
|
|
15
|
-
- Session startup sequence
|
|
16
|
-
- Progressive tool disclosure pattern
|
|
17
|
-
- Progressive skill disclosure pattern
|
|
18
|
-
- Critical rules for task management
|
|
11
|
+
Provides minimal guidance for progressive tool disclosure, caching, and task rules.
|
|
12
|
+
Startup sequence and skill discovery are now handled via workflow injection.
|
|
19
13
|
|
|
20
14
|
Returns:
|
|
21
|
-
XML-structured instructions string
|
|
15
|
+
XML-structured instructions string (~120 tokens)
|
|
22
16
|
"""
|
|
23
17
|
return """<gobby_system>
|
|
24
18
|
|
|
25
|
-
<startup>
|
|
26
|
-
At the start of EVERY session:
|
|
27
|
-
1. `list_mcp_servers()` — Discover available servers
|
|
28
|
-
2. `list_skills()` — Discover available skills
|
|
29
|
-
3. Session ID: Look for `Gobby Session Ref:` or `Gobby Session ID:` in your context.
|
|
30
|
-
If missing, call:
|
|
31
|
-
`call_tool("gobby-sessions", "get_current_session", {"external_id": "<your-session-id>", "source": "<cli-name>"})`
|
|
32
|
-
|
|
33
|
-
Session and task references use `#N` format (e.g., `#1`, `#42`) which is project-scoped.
|
|
34
|
-
</startup>
|
|
35
|
-
|
|
36
19
|
<tool_discovery>
|
|
37
20
|
NEVER assume tool schemas. Use progressive disclosure:
|
|
38
21
|
1. `list_tools(server="...")` — Lightweight metadata (~100 tokens/tool)
|
|
@@ -40,12 +23,11 @@ NEVER assume tool schemas. Use progressive disclosure:
|
|
|
40
23
|
3. `call_tool(server, tool, args)` — Execute
|
|
41
24
|
</tool_discovery>
|
|
42
25
|
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
</skill_discovery>
|
|
26
|
+
<caching>
|
|
27
|
+
Schema fetches are cached per session. Once you call `get_tool_schema(server, tool)`,
|
|
28
|
+
you can `call_tool` that same server:tool repeatedly WITHOUT re-fetching the schema.
|
|
29
|
+
Do NOT call list_tools or get_tool_schema before every call_tool — only on first use.
|
|
30
|
+
</caching>
|
|
49
31
|
|
|
50
32
|
<rules>
|
|
51
33
|
- Create/claim a task before using Edit, Write, or NotebookEdit tools
|
gobby/mcp_proxy/manager.py
CHANGED
|
@@ -684,6 +684,12 @@ class MCPClientManager:
|
|
|
684
684
|
|
|
685
685
|
async def get_tool_input_schema(self, server_name: str, tool_name: str) -> dict[str, Any]:
|
|
686
686
|
"""Get full inputSchema for a specific tool."""
|
|
687
|
+
tool_info = await self.get_tool_info(server_name, tool_name)
|
|
688
|
+
input_schema = tool_info.get("inputSchema", {})
|
|
689
|
+
return cast(dict[str, Any], input_schema)
|
|
690
|
+
|
|
691
|
+
async def get_tool_info(self, server_name: str, tool_name: str) -> dict[str, Any]:
|
|
692
|
+
"""Get full tool info including name, description, and inputSchema."""
|
|
687
693
|
|
|
688
694
|
# This is an optimization. Instead of calling list_tools again,
|
|
689
695
|
# we try to fetch it. But standard MCP list_tools returns everything.
|
|
@@ -696,9 +702,13 @@ class MCPClientManager:
|
|
|
696
702
|
# tool might be an object or dict
|
|
697
703
|
t_name = getattr(tool, "name", tool.get("name") if isinstance(tool, dict) else None)
|
|
698
704
|
if t_name == tool_name:
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
705
|
+
if isinstance(tool, dict):
|
|
706
|
+
result: dict[str, Any] = {"name": t_name}
|
|
707
|
+
if "description" in tool and tool["description"]:
|
|
708
|
+
result["description"] = tool["description"]
|
|
709
|
+
if "inputSchema" in tool:
|
|
710
|
+
result["inputSchema"] = tool["inputSchema"]
|
|
711
|
+
return result
|
|
702
712
|
|
|
703
713
|
raise MCPError(f"Tool {tool_name} not found on server {server_name}")
|
|
704
714
|
|
gobby/mcp_proxy/models.py
CHANGED
|
@@ -41,6 +41,7 @@ class ToolProxyErrorCode(str, Enum):
|
|
|
41
41
|
SERVER_NOT_FOUND = "SERVER_NOT_FOUND"
|
|
42
42
|
SERVER_NOT_CONFIGURED = "SERVER_NOT_CONFIGURED"
|
|
43
43
|
TOOL_NOT_FOUND = "TOOL_NOT_FOUND"
|
|
44
|
+
TOOL_BLOCKED = "TOOL_BLOCKED"
|
|
44
45
|
INVALID_ARGUMENTS = "INVALID_ARGUMENTS"
|
|
45
46
|
EXECUTION_ERROR = "EXECUTION_ERROR"
|
|
46
47
|
CONNECTION_ERROR = "CONNECTION_ERROR"
|
gobby/mcp_proxy/registries.py
CHANGED
|
@@ -17,14 +17,18 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from gobby.memory.manager import MemoryManager
|
|
18
18
|
from gobby.sessions.manager import SessionManager
|
|
19
19
|
from gobby.storage.clones import LocalCloneManager
|
|
20
|
+
from gobby.storage.database import DatabaseProtocol
|
|
20
21
|
from gobby.storage.inter_session_messages import InterSessionMessageManager
|
|
21
22
|
from gobby.storage.merge_resolutions import MergeResolutionManager
|
|
23
|
+
from gobby.storage.pipelines import LocalPipelineExecutionManager
|
|
22
24
|
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
23
25
|
from gobby.storage.sessions import LocalSessionManager
|
|
24
26
|
from gobby.storage.tasks import LocalTaskManager
|
|
25
27
|
from gobby.storage.worktrees import LocalWorktreeManager
|
|
26
28
|
from gobby.sync.tasks import TaskSyncManager
|
|
27
29
|
from gobby.tasks.validation import TaskValidator
|
|
30
|
+
from gobby.workflows.loader import WorkflowLoader
|
|
31
|
+
from gobby.workflows.pipeline_executor import PipelineExecutor
|
|
28
32
|
from gobby.worktrees.git import WorktreeGitManager
|
|
29
33
|
from gobby.worktrees.merge import MergeResolver
|
|
30
34
|
|
|
@@ -36,6 +40,7 @@ def setup_internal_registries(
|
|
|
36
40
|
_session_manager: SessionManager | None = None,
|
|
37
41
|
memory_manager: MemoryManager | None = None,
|
|
38
42
|
task_manager: LocalTaskManager | None = None,
|
|
43
|
+
db: DatabaseProtocol | None = None,
|
|
39
44
|
sync_manager: TaskSyncManager | None = None,
|
|
40
45
|
task_validator: TaskValidator | None = None,
|
|
41
46
|
message_manager: LocalSessionMessageManager | None = None,
|
|
@@ -51,6 +56,9 @@ def setup_internal_registries(
|
|
|
51
56
|
project_id: str | None = None,
|
|
52
57
|
tool_proxy_getter: Callable[[], ToolProxyService | None] | None = None,
|
|
53
58
|
inter_session_message_manager: InterSessionMessageManager | None = None,
|
|
59
|
+
pipeline_executor: PipelineExecutor | None = None,
|
|
60
|
+
workflow_loader: WorkflowLoader | None = None,
|
|
61
|
+
pipeline_execution_manager: LocalPipelineExecutionManager | None = None,
|
|
54
62
|
) -> InternalRegistryManager:
|
|
55
63
|
"""
|
|
56
64
|
Setup internal MCP registries (tasks, messages, memory, metrics, agents, worktrees).
|
|
@@ -60,6 +68,7 @@ def setup_internal_registries(
|
|
|
60
68
|
_session_manager: Session manager (reserved for future use)
|
|
61
69
|
memory_manager: Memory manager for memory operations
|
|
62
70
|
task_manager: Task storage manager
|
|
71
|
+
db: Database connection for registries that only need storage (skills, artifacts)
|
|
63
72
|
sync_manager: Task sync manager for git sync
|
|
64
73
|
task_validator: Task validator for validation
|
|
65
74
|
message_manager: Message storage manager
|
|
@@ -75,6 +84,9 @@ def setup_internal_registries(
|
|
|
75
84
|
tool_proxy_getter: Callable that returns ToolProxyService for routing
|
|
76
85
|
tool calls in in-process agents. Called lazily during agent execution.
|
|
77
86
|
inter_session_message_manager: Inter-session message manager for agent messaging
|
|
87
|
+
pipeline_executor: Pipeline executor for running pipelines
|
|
88
|
+
workflow_loader: Workflow loader for loading pipeline definitions
|
|
89
|
+
pipeline_execution_manager: Pipeline execution manager for tracking executions
|
|
78
90
|
|
|
79
91
|
Returns:
|
|
80
92
|
InternalRegistryManager containing all registries
|
|
@@ -143,6 +155,7 @@ def setup_internal_registries(
|
|
|
143
155
|
|
|
144
156
|
workflows_registry = create_workflows_registry(
|
|
145
157
|
session_manager=local_session_manager,
|
|
158
|
+
db=getattr(local_session_manager, "db", None) if local_session_manager else None,
|
|
146
159
|
)
|
|
147
160
|
manager.add_registry(workflows_registry)
|
|
148
161
|
logger.debug("Workflows registry initialized")
|
|
@@ -190,6 +203,9 @@ def setup_internal_registries(
|
|
|
190
203
|
git_manager=git_manager,
|
|
191
204
|
clone_storage=clone_storage,
|
|
192
205
|
clone_manager=clone_git_manager,
|
|
206
|
+
# For mode=self (workflow activation on caller session)
|
|
207
|
+
workflow_loader=workflow_loader,
|
|
208
|
+
db=db,
|
|
193
209
|
)
|
|
194
210
|
|
|
195
211
|
# Add inter-agent messaging tools if message manager and session manager are available
|
|
@@ -267,19 +283,64 @@ def setup_internal_registries(
|
|
|
267
283
|
manager.add_registry(hub_registry)
|
|
268
284
|
logger.debug("Hub registry initialized")
|
|
269
285
|
|
|
270
|
-
# Initialize skills registry
|
|
271
|
-
|
|
272
|
-
|
|
286
|
+
# Initialize skills registry if database is available
|
|
287
|
+
if db is not None:
|
|
288
|
+
from gobby.config.skills import SkillsConfig
|
|
273
289
|
from gobby.mcp_proxy.tools.skills import create_skills_registry
|
|
290
|
+
from gobby.skills.hubs import (
|
|
291
|
+
ClaudePluginsProvider,
|
|
292
|
+
ClawdHubProvider,
|
|
293
|
+
GitHubCollectionProvider,
|
|
294
|
+
HubManager,
|
|
295
|
+
SkillHubProvider,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Get skills config (or use defaults)
|
|
299
|
+
skills_config = _config.skills if _config and hasattr(_config, "skills") else SkillsConfig()
|
|
300
|
+
|
|
301
|
+
# Create hub manager with configured hubs
|
|
302
|
+
hub_manager = HubManager(configs=skills_config.hubs)
|
|
303
|
+
|
|
304
|
+
# Register provider factories
|
|
305
|
+
hub_manager.register_provider_factory("clawdhub", ClawdHubProvider)
|
|
306
|
+
hub_manager.register_provider_factory("skillhub", SkillHubProvider)
|
|
307
|
+
hub_manager.register_provider_factory("github-collection", GitHubCollectionProvider)
|
|
308
|
+
hub_manager.register_provider_factory("claude-plugins", ClaudePluginsProvider)
|
|
274
309
|
|
|
275
310
|
skills_registry = create_skills_registry(
|
|
276
|
-
db=
|
|
311
|
+
db=db,
|
|
277
312
|
project_id=project_id,
|
|
313
|
+
hub_manager=hub_manager,
|
|
278
314
|
)
|
|
279
315
|
manager.add_registry(skills_registry)
|
|
280
316
|
logger.debug("Skills registry initialized")
|
|
281
317
|
else:
|
|
282
|
-
logger.debug("Skills registry not initialized:
|
|
318
|
+
logger.debug("Skills registry not initialized: db is None")
|
|
319
|
+
|
|
320
|
+
# Initialize artifacts registry if database is available
|
|
321
|
+
if db is not None:
|
|
322
|
+
from gobby.mcp_proxy.tools.artifacts import create_artifacts_registry
|
|
323
|
+
|
|
324
|
+
artifacts_registry = create_artifacts_registry(
|
|
325
|
+
db=db,
|
|
326
|
+
session_manager=local_session_manager,
|
|
327
|
+
)
|
|
328
|
+
manager.add_registry(artifacts_registry)
|
|
329
|
+
logger.debug("Artifacts registry initialized")
|
|
330
|
+
else:
|
|
331
|
+
logger.debug("Artifacts registry not initialized: db is None")
|
|
332
|
+
|
|
333
|
+
# Initialize pipelines registry if pipeline_executor is available
|
|
334
|
+
if pipeline_executor is not None:
|
|
335
|
+
from gobby.mcp_proxy.tools.pipelines import create_pipelines_registry
|
|
336
|
+
|
|
337
|
+
pipelines_registry = create_pipelines_registry(
|
|
338
|
+
loader=workflow_loader,
|
|
339
|
+
executor=pipeline_executor,
|
|
340
|
+
execution_manager=pipeline_execution_manager,
|
|
341
|
+
)
|
|
342
|
+
manager.add_registry(pipelines_registry)
|
|
343
|
+
logger.debug("Pipelines registry initialized")
|
|
283
344
|
|
|
284
345
|
logger.info(f"Internal registries initialized: {len(manager)} registries")
|
|
285
346
|
return manager
|
gobby/mcp_proxy/server.py
CHANGED
|
@@ -98,6 +98,7 @@ class GobbyDaemonTools:
|
|
|
98
98
|
server_name: str,
|
|
99
99
|
tool_name: str,
|
|
100
100
|
arguments: dict[str, Any] | None = None,
|
|
101
|
+
session_id: str | None = None,
|
|
101
102
|
) -> Any:
|
|
102
103
|
"""Call a tool.
|
|
103
104
|
|
|
@@ -105,8 +106,11 @@ class GobbyDaemonTools:
|
|
|
105
106
|
underlying service indicates an error. This ensures the MCP protocol
|
|
106
107
|
properly signals errors to LLM clients instead of returning error dicts
|
|
107
108
|
as successful responses.
|
|
109
|
+
|
|
110
|
+
When session_id is provided and a workflow is active, checks that the
|
|
111
|
+
tool is not blocked by the current workflow step's blocked_tools setting.
|
|
108
112
|
"""
|
|
109
|
-
result = await self.tool_proxy.call_tool(server_name, tool_name, arguments)
|
|
113
|
+
result = await self.tool_proxy.call_tool(server_name, tool_name, arguments, session_id)
|
|
110
114
|
|
|
111
115
|
# Check if result indicates an error (ToolProxyService returns dict with success: False)
|
|
112
116
|
if isinstance(result, dict) and result.get("success") is False:
|
|
@@ -382,7 +386,7 @@ class GobbyDaemonTools:
|
|
|
382
386
|
|
|
383
387
|
Args:
|
|
384
388
|
event_type: Hook event type (e.g., "session_start", "before_tool")
|
|
385
|
-
source: Source CLI to simulate (claude, gemini, codex)
|
|
389
|
+
source: Source CLI to simulate (claude, gemini, codex, cursor, windsurf, copilot)
|
|
386
390
|
data: Optional additional data for the event
|
|
387
391
|
|
|
388
392
|
Returns:
|
|
@@ -15,22 +15,6 @@ logger = logging.getLogger("gobby.mcp.server")
|
|
|
15
15
|
# Search mode type
|
|
16
16
|
SearchMode = Literal["llm", "semantic", "hybrid"]
|
|
17
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
|
-
|
|
34
18
|
|
|
35
19
|
class RecommendationService:
|
|
36
20
|
"""Service for recommending tools."""
|
|
@@ -49,10 +33,6 @@ class RecommendationService:
|
|
|
49
33
|
self._project_id = project_id
|
|
50
34
|
self._config = config
|
|
51
35
|
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)
|
|
56
36
|
|
|
57
37
|
def _get_config(self) -> RecommendToolsConfig:
|
|
58
38
|
"""Get config with fallback to defaults."""
|
|
@@ -181,10 +161,7 @@ class RecommendationService:
|
|
|
181
161
|
"candidate_list": candidate_list,
|
|
182
162
|
"top_k": top_k,
|
|
183
163
|
}
|
|
184
|
-
|
|
185
|
-
prompt = self._loader.render(prompt_path, context)
|
|
186
|
-
except Exception:
|
|
187
|
-
prompt = DEFAULT_HYBRID_RERANK_PROMPT.format(**context)
|
|
164
|
+
prompt = self._loader.render(prompt_path, context)
|
|
188
165
|
|
|
189
166
|
provider = self._llm_service.get_default_provider()
|
|
190
167
|
response = await provider.generate_text(prompt)
|
|
@@ -223,10 +200,7 @@ class RecommendationService:
|
|
|
223
200
|
"task_description": task_description,
|
|
224
201
|
"available_servers": ", ".join(available_servers),
|
|
225
202
|
}
|
|
226
|
-
|
|
227
|
-
prompt = self._loader.render(prompt_path, context)
|
|
228
|
-
except Exception:
|
|
229
|
-
prompt = DEFAULT_LLM_PROMPT.format(**context)
|
|
203
|
+
prompt = self._loader.render(prompt_path, context)
|
|
230
204
|
|
|
231
205
|
provider = self._llm_service.get_default_provider()
|
|
232
206
|
response = await provider.generate_text(prompt)
|
|
@@ -4,6 +4,8 @@ import logging
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
+
from gobby.workflows.definitions import WorkflowDefinition
|
|
8
|
+
|
|
7
9
|
if TYPE_CHECKING:
|
|
8
10
|
from gobby.storage.database import LocalDatabase
|
|
9
11
|
from gobby.workflows.loader import WorkflowLoader
|
|
@@ -89,6 +91,11 @@ class ToolFilterService:
|
|
|
89
91
|
logger.warning(f"Workflow '{state.workflow_name}' not found")
|
|
90
92
|
return None
|
|
91
93
|
|
|
94
|
+
# Tool filtering only applies to step-based workflows
|
|
95
|
+
if not isinstance(definition, WorkflowDefinition):
|
|
96
|
+
logger.debug(f"Workflow '{state.workflow_name}' is not a step-based workflow")
|
|
97
|
+
return None
|
|
98
|
+
|
|
92
99
|
step = definition.get_step(state.step)
|
|
93
100
|
if not step:
|
|
94
101
|
logger.warning(f"Step '{state.step}' not found in workflow '{state.workflow_name}'")
|
|
@@ -208,6 +208,7 @@ class ToolProxyService:
|
|
|
208
208
|
server_name: str,
|
|
209
209
|
tool_name: str,
|
|
210
210
|
arguments: dict[str, Any] | None = None,
|
|
211
|
+
session_id: str | None = None,
|
|
211
212
|
) -> Any:
|
|
212
213
|
"""Execute a tool with optional pre-validation.
|
|
213
214
|
|
|
@@ -218,9 +219,24 @@ class ToolProxyService:
|
|
|
218
219
|
On execution error, includes fallback_suggestions if a fallback resolver
|
|
219
220
|
is configured.
|
|
220
221
|
|
|
222
|
+
When session_id is provided and a workflow is active, checks that the
|
|
223
|
+
tool is not blocked by the current workflow step's blocked_tools setting.
|
|
224
|
+
|
|
221
225
|
"""
|
|
222
226
|
args = arguments or {}
|
|
223
227
|
|
|
228
|
+
# Check workflow tool restrictions if session_id provided
|
|
229
|
+
if session_id and self._tool_filter:
|
|
230
|
+
is_allowed, reason = self._tool_filter.is_tool_allowed(tool_name, session_id)
|
|
231
|
+
if not is_allowed:
|
|
232
|
+
return {
|
|
233
|
+
"success": False,
|
|
234
|
+
"error": reason,
|
|
235
|
+
"error_code": ToolProxyErrorCode.TOOL_BLOCKED.value,
|
|
236
|
+
"server_name": server_name,
|
|
237
|
+
"tool_name": tool_name,
|
|
238
|
+
}
|
|
239
|
+
|
|
224
240
|
# Pre-validate arguments if enabled
|
|
225
241
|
if self._validate_arguments and args:
|
|
226
242
|
schema_result = await self.get_tool_schema(server_name, tool_name)
|
|
@@ -361,6 +377,7 @@ class ToolProxyService:
|
|
|
361
377
|
self,
|
|
362
378
|
tool_name: str,
|
|
363
379
|
arguments: dict[str, Any] | None = None,
|
|
380
|
+
session_id: str | None = None,
|
|
364
381
|
) -> Any:
|
|
365
382
|
"""
|
|
366
383
|
Call a tool by name, automatically resolving the server.
|
|
@@ -371,6 +388,7 @@ class ToolProxyService:
|
|
|
371
388
|
Args:
|
|
372
389
|
tool_name: Name of the tool to call
|
|
373
390
|
arguments: Tool arguments
|
|
391
|
+
session_id: Optional session ID for workflow tool restriction checks
|
|
374
392
|
|
|
375
393
|
Returns:
|
|
376
394
|
Tool execution result, or error dict if tool not found
|
|
@@ -386,4 +404,4 @@ class ToolProxyService:
|
|
|
386
404
|
}
|
|
387
405
|
|
|
388
406
|
logger.debug(f"Routing tool '{tool_name}' to server '{server_name}'")
|
|
389
|
-
return await self.call_tool(server_name, tool_name, arguments)
|
|
407
|
+
return await self.call_tool(server_name, tool_name, arguments, session_id)
|