gobby 0.2.5__py3-none-any.whl → 0.2.6__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/adapters/claude_code.py +13 -4
- gobby/adapters/codex.py +43 -3
- gobby/agents/runner.py +8 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +9 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +2 -8
- gobby/cli/installers/shared.py +71 -8
- 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 +3 -3
- 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/app.py +63 -1
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +6 -14
- gobby/hooks/event_handlers.py +145 -2
- gobby/hooks/hook_manager.py +48 -2
- gobby/hooks/skill_manager.py +130 -0
- 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 +95 -33
- gobby/mcp_proxy/instructions.py +54 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -5
- gobby/mcp_proxy/server.py +33 -3
- 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/clones.py +903 -0
- gobby/mcp_proxy/tools/memory.py +1 -24
- 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/session_messages.py +1 -2
- gobby/mcp_proxy/tools/skills/__init__.py +631 -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 +60 -29
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +5 -0
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/manager.py +11 -2
- gobby/prompts/defaults/handoff/compact.md +63 -0
- gobby/prompts/defaults/handoff/session_end.md +57 -0
- gobby/prompts/defaults/memory/extract.md +61 -0
- 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 +23 -8
- gobby/servers/routes/admin.py +280 -0
- gobby/servers/routes/mcp/tools.py +241 -52
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +64 -5
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +258 -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 +180 -6
- gobby/storage/sessions.py +73 -0
- gobby/storage/skills.py +749 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +41 -6
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +39 -4
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/validation.py +24 -15
- 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/workflows/actions.py +84 -2
- gobby/workflows/context_actions.py +43 -0
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/engine.py +13 -2
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/loader.py +19 -6
- gobby/workflows/memory_actions.py +74 -0
- gobby/workflows/summary_actions.py +17 -0
- gobby/workflows/task_enforcement_actions.py +448 -6
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
- 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/task_expansion.py +0 -591
- 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.6.dist-info}/WHEEL +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/top_level.txt +0 -0
gobby/llm/resolver.py
CHANGED
|
@@ -259,8 +259,13 @@ def create_executor(
|
|
|
259
259
|
"""
|
|
260
260
|
Create an AgentExecutor for the given provider.
|
|
261
261
|
|
|
262
|
+
Routing strategy:
|
|
263
|
+
- api_key and adc auth modes: Route to LiteLLMExecutor for unified cost tracking
|
|
264
|
+
- subscription mode (Claude): Route to ClaudeExecutor (Claude Agent SDK)
|
|
265
|
+
- cli mode (Codex): Route to CodexExecutor (Codex CLI subprocess)
|
|
266
|
+
|
|
262
267
|
Args:
|
|
263
|
-
provider: Provider name (claude, gemini, litellm).
|
|
268
|
+
provider: Provider name (claude, gemini, litellm, codex).
|
|
264
269
|
config: Optional daemon config for provider settings.
|
|
265
270
|
model: Optional model override.
|
|
266
271
|
|
|
@@ -279,19 +284,40 @@ def create_executor(
|
|
|
279
284
|
if config and config.llm_providers:
|
|
280
285
|
provider_config = getattr(config.llm_providers, provider, None)
|
|
281
286
|
|
|
287
|
+
# Determine auth_mode from config
|
|
288
|
+
auth_mode = "api_key" # Default
|
|
289
|
+
if provider_config:
|
|
290
|
+
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
291
|
+
|
|
282
292
|
try:
|
|
283
|
-
|
|
293
|
+
# Route based on auth_mode:
|
|
294
|
+
# - subscription (Claude) -> ClaudeExecutor
|
|
295
|
+
# - cli (Codex) -> CodexExecutor
|
|
296
|
+
# - api_key/adc (all providers) -> LiteLLMExecutor
|
|
297
|
+
|
|
298
|
+
if provider == "claude" and auth_mode == "subscription":
|
|
299
|
+
# Subscription mode requires Claude Agent SDK
|
|
284
300
|
return _create_claude_executor(provider_config, model)
|
|
285
|
-
|
|
286
|
-
|
|
301
|
+
|
|
302
|
+
elif provider == "codex" and auth_mode in ("subscription", "cli"):
|
|
303
|
+
# CLI mode requires Codex CLI subprocess
|
|
304
|
+
return _create_codex_executor(provider_config, model, auth_mode)
|
|
305
|
+
|
|
287
306
|
elif provider == "litellm":
|
|
307
|
+
# Direct LiteLLM usage
|
|
288
308
|
return _create_litellm_executor(provider_config, config, model)
|
|
289
|
-
|
|
290
|
-
|
|
309
|
+
|
|
310
|
+
elif auth_mode in ("api_key", "adc"):
|
|
311
|
+
# Route all api_key and adc modes through LiteLLM for unified cost tracking
|
|
312
|
+
return _create_litellm_executor_for_provider(
|
|
313
|
+
provider, auth_mode, provider_config, config, model
|
|
314
|
+
)
|
|
315
|
+
|
|
291
316
|
else:
|
|
292
317
|
raise ExecutorCreationError(
|
|
293
318
|
provider,
|
|
294
|
-
f"Unknown provider
|
|
319
|
+
f"Unknown provider/auth_mode combination: {provider}/{auth_mode}. "
|
|
320
|
+
f"Supported: {list(SUPPORTED_PROVIDERS)}",
|
|
295
321
|
)
|
|
296
322
|
except ProviderError:
|
|
297
323
|
raise
|
|
@@ -303,15 +329,18 @@ def _create_claude_executor(
|
|
|
303
329
|
provider_config: "LLMProviderConfig | None",
|
|
304
330
|
model: str | None,
|
|
305
331
|
) -> AgentExecutor:
|
|
306
|
-
"""
|
|
332
|
+
"""
|
|
333
|
+
Create ClaudeExecutor for subscription mode only.
|
|
334
|
+
|
|
335
|
+
Note: api_key mode is now routed through LiteLLMExecutor for unified cost tracking.
|
|
336
|
+
This function should only be called when auth_mode is "subscription".
|
|
337
|
+
"""
|
|
307
338
|
from gobby.llm.claude_executor import ClaudeExecutor
|
|
308
339
|
|
|
309
|
-
#
|
|
310
|
-
auth_mode = "api_key"
|
|
340
|
+
# Subscription mode only - api_key mode routes through LiteLLM
|
|
311
341
|
default_model = "claude-sonnet-4-20250514"
|
|
312
342
|
|
|
313
343
|
if provider_config:
|
|
314
|
-
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
315
344
|
# Get first model from comma-separated list if set
|
|
316
345
|
models_str = getattr(provider_config, "models", None)
|
|
317
346
|
if models_str:
|
|
@@ -320,46 +349,71 @@ def _create_claude_executor(
|
|
|
320
349
|
default_model = models[0]
|
|
321
350
|
|
|
322
351
|
return ClaudeExecutor(
|
|
323
|
-
auth_mode=
|
|
352
|
+
auth_mode="subscription",
|
|
324
353
|
default_model=model or default_model,
|
|
325
354
|
)
|
|
326
355
|
|
|
327
356
|
|
|
328
|
-
def
|
|
357
|
+
def _create_litellm_executor(
|
|
329
358
|
provider_config: "LLMProviderConfig | None",
|
|
359
|
+
config: "DaemonConfig | None",
|
|
330
360
|
model: str | None,
|
|
331
361
|
) -> AgentExecutor:
|
|
332
|
-
"""Create
|
|
333
|
-
from gobby.llm.
|
|
362
|
+
"""Create LiteLLMExecutor with API keys from config (direct litellm usage)."""
|
|
363
|
+
from gobby.llm.litellm_executor import LiteLLMExecutor
|
|
334
364
|
|
|
335
|
-
# Determine
|
|
336
|
-
|
|
337
|
-
|
|
365
|
+
# Determine model and API base from config
|
|
366
|
+
default_model = "gpt-4o-mini"
|
|
367
|
+
api_base = None
|
|
368
|
+
api_keys: dict[str, str] | None = None
|
|
338
369
|
|
|
339
370
|
if provider_config:
|
|
340
|
-
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
341
371
|
models_str = getattr(provider_config, "models", None)
|
|
342
372
|
if models_str:
|
|
343
373
|
models = [m.strip() for m in models_str.split(",") if m.strip()]
|
|
344
374
|
if models:
|
|
345
375
|
default_model = models[0]
|
|
376
|
+
api_base = getattr(provider_config, "api_base", None)
|
|
346
377
|
|
|
347
|
-
|
|
348
|
-
|
|
378
|
+
# Get API keys from llm_providers.api_keys
|
|
379
|
+
if config and config.llm_providers:
|
|
380
|
+
api_keys = config.llm_providers.api_keys or None
|
|
381
|
+
|
|
382
|
+
return LiteLLMExecutor(
|
|
349
383
|
default_model=model or default_model,
|
|
384
|
+
api_base=api_base,
|
|
385
|
+
api_keys=api_keys,
|
|
350
386
|
)
|
|
351
387
|
|
|
352
388
|
|
|
353
|
-
def
|
|
389
|
+
def _create_litellm_executor_for_provider(
|
|
390
|
+
provider: str,
|
|
391
|
+
auth_mode: str,
|
|
354
392
|
provider_config: "LLMProviderConfig | None",
|
|
355
393
|
config: "DaemonConfig | None",
|
|
356
394
|
model: str | None,
|
|
357
395
|
) -> AgentExecutor:
|
|
358
|
-
"""
|
|
396
|
+
"""
|
|
397
|
+
Create LiteLLMExecutor configured for a specific provider's api_key/adc mode.
|
|
398
|
+
|
|
399
|
+
This routes provider-specific calls through LiteLLM for unified cost tracking:
|
|
400
|
+
- Claude (api_key) -> anthropic/model
|
|
401
|
+
- Gemini (api_key) -> gemini/model
|
|
402
|
+
- Gemini (adc) -> vertex_ai/model
|
|
403
|
+
- Codex/OpenAI (api_key) -> model (no prefix)
|
|
404
|
+
"""
|
|
359
405
|
from gobby.llm.litellm_executor import LiteLLMExecutor
|
|
360
406
|
|
|
361
|
-
#
|
|
362
|
-
|
|
407
|
+
# Default models per provider
|
|
408
|
+
default_models = {
|
|
409
|
+
"claude": "claude-sonnet-4-20250514",
|
|
410
|
+
"gemini": "gemini-2.0-flash",
|
|
411
|
+
"codex": "gpt-4o",
|
|
412
|
+
"openai": "gpt-4o",
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# Determine model from config
|
|
416
|
+
default_model = default_models.get(provider, "gpt-4o-mini")
|
|
363
417
|
api_base = None
|
|
364
418
|
api_keys: dict[str, str] | None = None
|
|
365
419
|
|
|
@@ -375,34 +429,42 @@ def _create_litellm_executor(
|
|
|
375
429
|
if config and config.llm_providers:
|
|
376
430
|
api_keys = config.llm_providers.api_keys or None
|
|
377
431
|
|
|
432
|
+
# Cast auth_mode to the expected literal type
|
|
433
|
+
litellm_auth_mode = auth_mode if auth_mode in ("api_key", "adc") else "api_key"
|
|
434
|
+
|
|
378
435
|
return LiteLLMExecutor(
|
|
379
436
|
default_model=model or default_model,
|
|
380
437
|
api_base=api_base,
|
|
381
438
|
api_keys=api_keys,
|
|
439
|
+
provider=provider, # type: ignore[arg-type]
|
|
440
|
+
auth_mode=litellm_auth_mode, # type: ignore[arg-type]
|
|
382
441
|
)
|
|
383
442
|
|
|
384
443
|
|
|
385
444
|
def _create_codex_executor(
|
|
386
445
|
provider_config: "LLMProviderConfig | None",
|
|
387
446
|
model: str | None,
|
|
447
|
+
auth_mode: str = "subscription",
|
|
388
448
|
) -> AgentExecutor:
|
|
389
449
|
"""
|
|
390
|
-
Create CodexExecutor
|
|
450
|
+
Create CodexExecutor for subscription/CLI mode only.
|
|
451
|
+
|
|
452
|
+
Note: api_key mode is now routed through LiteLLMExecutor for unified cost tracking.
|
|
453
|
+
This function should only be called when auth_mode is "subscription" or "cli".
|
|
391
454
|
|
|
392
|
-
Codex
|
|
393
|
-
- api_key: OpenAI API with function calling (full tool injection)
|
|
394
|
-
- subscription: Codex CLI with ChatGPT subscription (no custom tools)
|
|
455
|
+
CLI mode uses Codex CLI subprocess - no custom tool injection supported.
|
|
395
456
|
|
|
396
|
-
|
|
457
|
+
Args:
|
|
458
|
+
provider_config: Provider configuration.
|
|
459
|
+
model: Optional model override.
|
|
460
|
+
auth_mode: Authentication mode - "subscription" or "cli".
|
|
397
461
|
"""
|
|
398
462
|
from gobby.llm.codex_executor import CodexExecutor
|
|
399
463
|
|
|
400
|
-
#
|
|
401
|
-
auth_mode = "api_key"
|
|
464
|
+
# CLI/subscription mode only - api_key mode routes through LiteLLM
|
|
402
465
|
default_model = "gpt-4o"
|
|
403
466
|
|
|
404
467
|
if provider_config:
|
|
405
|
-
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
406
468
|
models_str = getattr(provider_config, "models", None)
|
|
407
469
|
if models_str:
|
|
408
470
|
models = [m.strip() for m in models_str.split(",") if m.strip()]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Gobby MCP server instructions.
|
|
2
|
+
|
|
3
|
+
Provides XML-structured instructions that teach agents how to use Gobby correctly.
|
|
4
|
+
These instructions are injected into the MCP server via FastMCP's `instructions` parameter.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_gobby_instructions() -> str:
|
|
9
|
+
"""Build XML-structured instructions for Gobby MCP server.
|
|
10
|
+
|
|
11
|
+
These instructions teach agents how to use Gobby correctly.
|
|
12
|
+
Every agent connecting to Gobby receives these automatically.
|
|
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
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
XML-structured instructions string
|
|
22
|
+
"""
|
|
23
|
+
return """<gobby_system>
|
|
24
|
+
|
|
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 `session_id: <uuid>` in your context.
|
|
30
|
+
If missing, call:
|
|
31
|
+
`call_tool("gobby-sessions", "get_current", {"external_id": "<your-session-id>", "source": "claude"})`
|
|
32
|
+
</startup>
|
|
33
|
+
|
|
34
|
+
<tool_discovery>
|
|
35
|
+
NEVER assume tool schemas. Use progressive disclosure:
|
|
36
|
+
1. `list_tools(server="...")` — Lightweight metadata (~100 tokens/tool)
|
|
37
|
+
2. `get_tool_schema(server, tool)` — Full schema when needed
|
|
38
|
+
3. `call_tool(server, tool, args)` — Execute
|
|
39
|
+
</tool_discovery>
|
|
40
|
+
|
|
41
|
+
<skill_discovery>
|
|
42
|
+
Skills provide detailed guidance. Use progressive disclosure:
|
|
43
|
+
1. `list_skills()` — Already done at startup
|
|
44
|
+
2. `get_skill(name="...")` — Full content when needed
|
|
45
|
+
3. `search_skills(query="...")` — Find by task description
|
|
46
|
+
</skill_discovery>
|
|
47
|
+
|
|
48
|
+
<rules>
|
|
49
|
+
- Create/claim a task before using Edit, Write, or NotebookEdit tools
|
|
50
|
+
- Pass session_id to create_task (required), claim_task (required), and close_task (optional, for tracking)
|
|
51
|
+
- NEVER load all tool schemas upfront — use progressive disclosure
|
|
52
|
+
</rules>
|
|
53
|
+
|
|
54
|
+
</gobby_system>"""
|
gobby/mcp_proxy/models.py
CHANGED
|
@@ -31,6 +31,21 @@ class MCPError(Exception):
|
|
|
31
31
|
self.code = code
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class ToolProxyErrorCode(str, Enum):
|
|
35
|
+
"""Structured error codes for ToolProxyService responses.
|
|
36
|
+
|
|
37
|
+
Used by _process_tool_proxy_result to determine HTTP status codes
|
|
38
|
+
without fragile string matching.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
SERVER_NOT_FOUND = "SERVER_NOT_FOUND"
|
|
42
|
+
SERVER_NOT_CONFIGURED = "SERVER_NOT_CONFIGURED"
|
|
43
|
+
TOOL_NOT_FOUND = "TOOL_NOT_FOUND"
|
|
44
|
+
INVALID_ARGUMENTS = "INVALID_ARGUMENTS"
|
|
45
|
+
EXECUTION_ERROR = "EXECUTION_ERROR"
|
|
46
|
+
CONNECTION_ERROR = "CONNECTION_ERROR"
|
|
47
|
+
|
|
48
|
+
|
|
34
49
|
class HealthState(str, Enum):
|
|
35
50
|
"""Connection health state for monitoring."""
|
|
36
51
|
|
gobby/mcp_proxy/registries.py
CHANGED
|
@@ -16,13 +16,14 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from gobby.mcp_proxy.services.tool_proxy import ToolProxyService
|
|
17
17
|
from gobby.memory.manager import MemoryManager
|
|
18
18
|
from gobby.sessions.manager import SessionManager
|
|
19
|
+
from gobby.storage.clones import LocalCloneManager
|
|
20
|
+
from gobby.storage.inter_session_messages import InterSessionMessageManager
|
|
19
21
|
from gobby.storage.merge_resolutions import MergeResolutionManager
|
|
20
22
|
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
21
23
|
from gobby.storage.sessions import LocalSessionManager
|
|
22
24
|
from gobby.storage.tasks import LocalTaskManager
|
|
23
25
|
from gobby.storage.worktrees import LocalWorktreeManager
|
|
24
26
|
from gobby.sync.tasks import TaskSyncManager
|
|
25
|
-
from gobby.tasks.expansion import TaskExpander
|
|
26
27
|
from gobby.tasks.validation import TaskValidator
|
|
27
28
|
from gobby.worktrees.git import WorktreeGitManager
|
|
28
29
|
from gobby.worktrees.merge import MergeResolver
|
|
@@ -36,7 +37,6 @@ def setup_internal_registries(
|
|
|
36
37
|
memory_manager: MemoryManager | None = None,
|
|
37
38
|
task_manager: LocalTaskManager | None = None,
|
|
38
39
|
sync_manager: TaskSyncManager | None = None,
|
|
39
|
-
task_expander: TaskExpander | None = None,
|
|
40
40
|
task_validator: TaskValidator | None = None,
|
|
41
41
|
message_manager: LocalSessionMessageManager | None = None,
|
|
42
42
|
local_session_manager: LocalSessionManager | None = None,
|
|
@@ -44,11 +44,13 @@ def setup_internal_registries(
|
|
|
44
44
|
llm_service: LLMService | None = None,
|
|
45
45
|
agent_runner: AgentRunner | None = None,
|
|
46
46
|
worktree_storage: LocalWorktreeManager | None = None,
|
|
47
|
+
clone_storage: LocalCloneManager | None = None,
|
|
47
48
|
git_manager: WorktreeGitManager | None = None,
|
|
48
49
|
merge_storage: MergeResolutionManager | None = None,
|
|
49
50
|
merge_resolver: MergeResolver | None = None,
|
|
50
51
|
project_id: str | None = None,
|
|
51
52
|
tool_proxy_getter: Callable[[], ToolProxyService | None] | None = None,
|
|
53
|
+
inter_session_message_manager: InterSessionMessageManager | None = None,
|
|
52
54
|
) -> InternalRegistryManager:
|
|
53
55
|
"""
|
|
54
56
|
Setup internal MCP registries (tasks, messages, memory, metrics, agents, worktrees).
|
|
@@ -59,7 +61,6 @@ def setup_internal_registries(
|
|
|
59
61
|
memory_manager: Memory manager for memory operations
|
|
60
62
|
task_manager: Task storage manager
|
|
61
63
|
sync_manager: Task sync manager for git sync
|
|
62
|
-
task_expander: Task expander for AI expansion
|
|
63
64
|
task_validator: Task validator for validation
|
|
64
65
|
message_manager: Message storage manager
|
|
65
66
|
local_session_manager: Local session manager for session CRUD
|
|
@@ -73,6 +74,7 @@ def setup_internal_registries(
|
|
|
73
74
|
project_id: Default project ID for worktree operations
|
|
74
75
|
tool_proxy_getter: Callable that returns ToolProxyService for routing
|
|
75
76
|
tool calls in in-process agents. Called lazily during agent execution.
|
|
77
|
+
inter_session_message_manager: Inter-session message manager for agent messaging
|
|
76
78
|
|
|
77
79
|
Returns:
|
|
78
80
|
InternalRegistryManager containing all registries
|
|
@@ -99,7 +101,6 @@ def setup_internal_registries(
|
|
|
99
101
|
tasks_registry = create_task_registry(
|
|
100
102
|
task_manager=task_manager,
|
|
101
103
|
sync_manager=sync_manager,
|
|
102
|
-
task_expander=task_expander,
|
|
103
104
|
task_validator=task_validator,
|
|
104
105
|
config=_config,
|
|
105
106
|
agent_runner=agent_runner,
|
|
@@ -150,20 +151,42 @@ def setup_internal_registries(
|
|
|
150
151
|
if metrics_manager is not None:
|
|
151
152
|
from gobby.mcp_proxy.tools.metrics import create_metrics_registry
|
|
152
153
|
|
|
154
|
+
# Get daily budget from conductor config if available
|
|
155
|
+
daily_budget_usd = 50.0 # Default
|
|
156
|
+
if _config is not None:
|
|
157
|
+
conductor_config = _config.conductor
|
|
158
|
+
if conductor_config is not None:
|
|
159
|
+
daily_budget_usd = conductor_config.daily_budget_usd
|
|
160
|
+
|
|
153
161
|
metrics_registry = create_metrics_registry(
|
|
154
162
|
metrics_manager=metrics_manager,
|
|
163
|
+
session_storage=local_session_manager,
|
|
164
|
+
daily_budget_usd=daily_budget_usd,
|
|
155
165
|
)
|
|
156
166
|
manager.add_registry(metrics_registry)
|
|
157
|
-
logger.debug("Metrics registry initialized")
|
|
167
|
+
logger.debug("Metrics registry initialized with token tracking")
|
|
158
168
|
|
|
159
169
|
# Initialize agents registry if agent_runner is available
|
|
160
170
|
if agent_runner is not None:
|
|
171
|
+
from gobby.agents.registry import get_running_agent_registry
|
|
161
172
|
from gobby.mcp_proxy.tools.agents import create_agents_registry
|
|
162
173
|
|
|
163
174
|
agents_registry = create_agents_registry(
|
|
164
175
|
runner=agent_runner,
|
|
165
176
|
tool_proxy_getter=tool_proxy_getter,
|
|
166
177
|
)
|
|
178
|
+
|
|
179
|
+
# Add inter-agent messaging tools if message manager is available
|
|
180
|
+
if inter_session_message_manager is not None:
|
|
181
|
+
from gobby.mcp_proxy.tools.agent_messaging import add_messaging_tools
|
|
182
|
+
|
|
183
|
+
add_messaging_tools(
|
|
184
|
+
registry=agents_registry,
|
|
185
|
+
message_manager=inter_session_message_manager,
|
|
186
|
+
agent_registry=get_running_agent_registry(),
|
|
187
|
+
)
|
|
188
|
+
logger.debug("Agent messaging tools added to agents registry")
|
|
189
|
+
|
|
167
190
|
manager.add_registry(agents_registry)
|
|
168
191
|
logger.debug("Agents registry initialized")
|
|
169
192
|
|
|
@@ -180,6 +203,32 @@ def setup_internal_registries(
|
|
|
180
203
|
manager.add_registry(worktrees_registry)
|
|
181
204
|
logger.debug("Worktrees registry initialized")
|
|
182
205
|
|
|
206
|
+
# Initialize clones registry if clone_storage is available
|
|
207
|
+
if clone_storage is not None:
|
|
208
|
+
from gobby.clones.git import CloneGitManager
|
|
209
|
+
from gobby.mcp_proxy.tools.clones import create_clones_registry
|
|
210
|
+
|
|
211
|
+
# Create CloneGitManager from the same repo path as WorktreeGitManager
|
|
212
|
+
clone_git_manager = None
|
|
213
|
+
if git_manager is not None:
|
|
214
|
+
try:
|
|
215
|
+
clone_git_manager = CloneGitManager(git_manager.repo_path)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.warning(f"Failed to create CloneGitManager: {e}")
|
|
218
|
+
|
|
219
|
+
# Only create clones registry if we have a git manager
|
|
220
|
+
if clone_git_manager is not None:
|
|
221
|
+
clones_registry = create_clones_registry(
|
|
222
|
+
clone_storage=clone_storage,
|
|
223
|
+
git_manager=clone_git_manager,
|
|
224
|
+
project_id=project_id or "",
|
|
225
|
+
agent_runner=agent_runner,
|
|
226
|
+
)
|
|
227
|
+
manager.add_registry(clones_registry)
|
|
228
|
+
logger.debug("Clones registry initialized")
|
|
229
|
+
else:
|
|
230
|
+
logger.debug("Clones registry not initialized: CloneGitManager not available")
|
|
231
|
+
|
|
183
232
|
# Initialize merge resolution registry if merge components are available
|
|
184
233
|
if merge_storage is not None and merge_resolver is not None:
|
|
185
234
|
from gobby.mcp_proxy.tools.merge import create_merge_registry
|
|
@@ -204,6 +253,20 @@ def setup_internal_registries(
|
|
|
204
253
|
manager.add_registry(hub_registry)
|
|
205
254
|
logger.debug("Hub registry initialized")
|
|
206
255
|
|
|
256
|
+
# Initialize skills registry using the existing database from task_manager
|
|
257
|
+
# to avoid creating a duplicate connection that would leak
|
|
258
|
+
if task_manager is not None:
|
|
259
|
+
from gobby.mcp_proxy.tools.skills import create_skills_registry
|
|
260
|
+
|
|
261
|
+
skills_registry = create_skills_registry(
|
|
262
|
+
db=task_manager.db,
|
|
263
|
+
project_id=project_id,
|
|
264
|
+
)
|
|
265
|
+
manager.add_registry(skills_registry)
|
|
266
|
+
logger.debug("Skills registry initialized")
|
|
267
|
+
else:
|
|
268
|
+
logger.debug("Skills registry not initialized: task_manager is None")
|
|
269
|
+
|
|
207
270
|
logger.info(f"Internal registries initialized: {len(manager)} registries")
|
|
208
271
|
return manager
|
|
209
272
|
|
gobby/mcp_proxy/server.py
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
Gobby Daemon Tools MCP Server.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
6
7
|
from datetime import UTC
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from mcp.server.fastmcp import FastMCP
|
|
11
|
+
from mcp.types import CallToolResult, TextContent
|
|
10
12
|
|
|
11
13
|
from gobby.config.app import DaemonConfig
|
|
14
|
+
from gobby.mcp_proxy.instructions import build_gobby_instructions
|
|
12
15
|
from gobby.mcp_proxy.manager import MCPClientManager
|
|
13
16
|
from gobby.mcp_proxy.services.recommendation import RecommendationService, SearchMode
|
|
14
17
|
from gobby.mcp_proxy.services.server_mgmt import ServerManagementService
|
|
@@ -96,8 +99,35 @@ class GobbyDaemonTools:
|
|
|
96
99
|
tool_name: str,
|
|
97
100
|
arguments: dict[str, Any] | None = None,
|
|
98
101
|
) -> Any:
|
|
99
|
-
"""Call a tool.
|
|
100
|
-
|
|
102
|
+
"""Call a tool.
|
|
103
|
+
|
|
104
|
+
Returns the tool result, or a CallToolResult with isError=True if the
|
|
105
|
+
underlying service indicates an error. This ensures the MCP protocol
|
|
106
|
+
properly signals errors to LLM clients instead of returning error dicts
|
|
107
|
+
as successful responses.
|
|
108
|
+
"""
|
|
109
|
+
result = await self.tool_proxy.call_tool(server_name, tool_name, arguments)
|
|
110
|
+
|
|
111
|
+
# Check if result indicates an error (ToolProxyService returns dict with success: False)
|
|
112
|
+
if isinstance(result, dict) and result.get("success") is False:
|
|
113
|
+
# Build helpful error message with schema hint if available
|
|
114
|
+
error_msg = result.get("error", "Unknown error")
|
|
115
|
+
hint = result.get("hint", "")
|
|
116
|
+
schema = result.get("schema")
|
|
117
|
+
|
|
118
|
+
parts = [f"Error: {error_msg}"]
|
|
119
|
+
if hint:
|
|
120
|
+
parts.append(f"\n{hint}")
|
|
121
|
+
if schema:
|
|
122
|
+
parts.append(f"\nCorrect schema:\n{json.dumps(schema, indent=2)}")
|
|
123
|
+
|
|
124
|
+
# Return MCP error response with isError=True
|
|
125
|
+
return CallToolResult(
|
|
126
|
+
content=[TextContent(type="text", text="\n".join(parts))],
|
|
127
|
+
isError=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return result
|
|
101
131
|
|
|
102
132
|
async def list_tools(self, server: str, session_id: str | None = None) -> dict[str, Any]:
|
|
103
133
|
"""List tools for a specific server, optionally filtered by workflow phase restrictions."""
|
|
@@ -513,7 +543,7 @@ class GobbyDaemonTools:
|
|
|
513
543
|
|
|
514
544
|
def create_mcp_server(tools_handler: GobbyDaemonTools) -> FastMCP:
|
|
515
545
|
"""Create the FastMCP server instance for the HTTP daemon."""
|
|
516
|
-
mcp = FastMCP("gobby")
|
|
546
|
+
mcp = FastMCP("gobby", instructions=build_gobby_instructions())
|
|
517
547
|
|
|
518
548
|
# System tools
|
|
519
549
|
mcp.add_tool(tools_handler.status)
|
|
@@ -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",
|