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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Session-based token tracking for budget management.
|
|
2
|
+
|
|
3
|
+
This module provides SessionTokenTracker which aggregates usage from sessions
|
|
4
|
+
over time and enables budget tracking for agent spawning decisions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import UTC, datetime, timedelta
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SessionTokenTracker:
|
|
19
|
+
"""Track token usage from sessions for budget management.
|
|
20
|
+
|
|
21
|
+
This class aggregates usage data from sessions over time and provides
|
|
22
|
+
budget checking for agent spawning decisions.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
tracker = SessionTokenTracker(
|
|
26
|
+
session_storage=session_manager,
|
|
27
|
+
daily_budget_usd=10.0,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Get usage summary for last 7 days
|
|
31
|
+
summary = tracker.get_usage_summary(days=7)
|
|
32
|
+
|
|
33
|
+
# Check if we can spawn an agent
|
|
34
|
+
can_spawn, reason = tracker.can_spawn_agent()
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
session_storage: Any # LocalSessionManager
|
|
38
|
+
daily_budget_usd: float = 50.0 # Default daily budget in USD
|
|
39
|
+
|
|
40
|
+
# Cached data
|
|
41
|
+
_usage_cache: dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
_cache_time: datetime | None = field(default=None)
|
|
43
|
+
_cache_ttl_seconds: int = 60 # Cache for 60 seconds
|
|
44
|
+
|
|
45
|
+
def get_usage_summary(self, days: int = 1) -> dict[str, Any]:
|
|
46
|
+
"""Get usage summary for the specified number of days.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
days: Number of days to look back (default: 1 = today)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dict with total cost, tokens, session count, and model breakdown
|
|
53
|
+
"""
|
|
54
|
+
since = datetime.now(UTC) - timedelta(days=days)
|
|
55
|
+
sessions = self.session_storage.get_sessions_since(since)
|
|
56
|
+
|
|
57
|
+
total_cost = 0.0
|
|
58
|
+
total_input_tokens = 0
|
|
59
|
+
total_output_tokens = 0
|
|
60
|
+
total_cache_creation_tokens = 0
|
|
61
|
+
total_cache_read_tokens = 0
|
|
62
|
+
usage_by_model: dict[str, dict[str, Any]] = {}
|
|
63
|
+
|
|
64
|
+
for session in sessions:
|
|
65
|
+
# Safely handle None values by defaulting to 0
|
|
66
|
+
total_cost += session.usage_total_cost_usd or 0
|
|
67
|
+
total_input_tokens += session.usage_input_tokens or 0
|
|
68
|
+
total_output_tokens += session.usage_output_tokens or 0
|
|
69
|
+
total_cache_creation_tokens += session.usage_cache_creation_tokens or 0
|
|
70
|
+
total_cache_read_tokens += session.usage_cache_read_tokens or 0
|
|
71
|
+
|
|
72
|
+
# Aggregate by model
|
|
73
|
+
model = session.model or "unknown"
|
|
74
|
+
if model not in usage_by_model:
|
|
75
|
+
usage_by_model[model] = {
|
|
76
|
+
"cost": 0.0,
|
|
77
|
+
"input_tokens": 0,
|
|
78
|
+
"output_tokens": 0,
|
|
79
|
+
"sessions": 0,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
usage_by_model[model]["cost"] += session.usage_total_cost_usd or 0
|
|
83
|
+
usage_by_model[model]["input_tokens"] += session.usage_input_tokens or 0
|
|
84
|
+
usage_by_model[model]["output_tokens"] += session.usage_output_tokens or 0
|
|
85
|
+
usage_by_model[model]["sessions"] += 1
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
"total_cost_usd": total_cost,
|
|
89
|
+
"total_input_tokens": total_input_tokens,
|
|
90
|
+
"total_output_tokens": total_output_tokens,
|
|
91
|
+
"total_cache_creation_tokens": total_cache_creation_tokens,
|
|
92
|
+
"total_cache_read_tokens": total_cache_read_tokens,
|
|
93
|
+
"session_count": len(sessions),
|
|
94
|
+
"usage_by_model": usage_by_model,
|
|
95
|
+
"period_days": days,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def get_budget_status(self) -> dict[str, Any]:
|
|
99
|
+
"""Get current budget status for today.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict with budget info: daily_budget_usd, used_today_usd,
|
|
103
|
+
remaining_usd, percentage_used, over_budget
|
|
104
|
+
"""
|
|
105
|
+
summary = self.get_usage_summary(days=1)
|
|
106
|
+
used_today = summary["total_cost_usd"]
|
|
107
|
+
|
|
108
|
+
# Handle unlimited budget (daily_budget_usd <= 0)
|
|
109
|
+
if self.daily_budget_usd <= 0:
|
|
110
|
+
return {
|
|
111
|
+
"daily_budget_usd": self.daily_budget_usd,
|
|
112
|
+
"used_today_usd": used_today,
|
|
113
|
+
"remaining_usd": float("inf"),
|
|
114
|
+
"percentage_used": 0.0,
|
|
115
|
+
"over_budget": False,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
remaining = self.daily_budget_usd - used_today
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"daily_budget_usd": self.daily_budget_usd,
|
|
122
|
+
"used_today_usd": used_today,
|
|
123
|
+
"remaining_usd": remaining,
|
|
124
|
+
"percentage_used": (used_today / self.daily_budget_usd * 100),
|
|
125
|
+
"over_budget": used_today > self.daily_budget_usd,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def can_spawn_agent(self, estimated_cost: float | None = None) -> tuple[bool, str | None]:
|
|
129
|
+
"""Check if we can spawn an agent based on budget.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
estimated_cost: Optional estimated cost for the agent run
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Tuple of (can_spawn, reason if not)
|
|
136
|
+
"""
|
|
137
|
+
# Unlimited budget (0 or negative means no limit)
|
|
138
|
+
if self.daily_budget_usd <= 0.0:
|
|
139
|
+
return True, None
|
|
140
|
+
|
|
141
|
+
status = self.get_budget_status()
|
|
142
|
+
|
|
143
|
+
# Already over budget
|
|
144
|
+
if status["over_budget"]:
|
|
145
|
+
return (
|
|
146
|
+
False,
|
|
147
|
+
f"Daily budget exceeded: ${status['used_today_usd']:.2f} used "
|
|
148
|
+
f"of ${self.daily_budget_usd:.2f}",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Check if estimated cost would exceed budget
|
|
152
|
+
if estimated_cost is not None:
|
|
153
|
+
if status["used_today_usd"] + estimated_cost > self.daily_budget_usd:
|
|
154
|
+
return (
|
|
155
|
+
False,
|
|
156
|
+
f"Estimated cost ${estimated_cost:.2f} would exceed budget. "
|
|
157
|
+
f"Remaining: ${status['remaining_usd']:.2f}",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return True, None
|
gobby/config/app.py
CHANGED
|
@@ -39,6 +39,7 @@ from gobby.config.persistence import (
|
|
|
39
39
|
MemoryConfig,
|
|
40
40
|
MemorySyncConfig,
|
|
41
41
|
)
|
|
42
|
+
from gobby.config.search import SearchConfig
|
|
42
43
|
from gobby.config.servers import MCPClientProxyConfig, WebSocketSettings
|
|
43
44
|
from gobby.config.sessions import (
|
|
44
45
|
ArtifactHandoffConfig,
|
|
@@ -48,6 +49,7 @@ from gobby.config.sessions import (
|
|
|
48
49
|
SessionSummaryConfig,
|
|
49
50
|
TitleSynthesisConfig,
|
|
50
51
|
)
|
|
52
|
+
from gobby.config.skills import SkillsConfig
|
|
51
53
|
from gobby.config.tasks import (
|
|
52
54
|
CompactHandoffConfig,
|
|
53
55
|
GobbyTasksConfig,
|
|
@@ -81,9 +83,13 @@ __all__ = [
|
|
|
81
83
|
# From gobby.config.persistence
|
|
82
84
|
"MemoryConfig",
|
|
83
85
|
"MemorySyncConfig",
|
|
86
|
+
# From gobby.config.search
|
|
87
|
+
"SearchConfig",
|
|
84
88
|
# From gobby.config.servers
|
|
85
89
|
"MCPClientProxyConfig",
|
|
86
90
|
"WebSocketSettings",
|
|
91
|
+
# From gobby.config.skills
|
|
92
|
+
"SkillsConfig",
|
|
87
93
|
# From gobby.config.sessions
|
|
88
94
|
"ArtifactHandoffConfig",
|
|
89
95
|
"ContextInjectionConfig",
|
|
@@ -99,6 +105,7 @@ __all__ = [
|
|
|
99
105
|
"TaskValidationConfig",
|
|
100
106
|
"WorkflowConfig",
|
|
101
107
|
# Local definitions
|
|
108
|
+
"ConductorConfig",
|
|
102
109
|
"DaemonConfig",
|
|
103
110
|
"expand_env_vars",
|
|
104
111
|
"load_yaml",
|
|
@@ -114,6 +121,37 @@ __all__ = [
|
|
|
114
121
|
ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}")
|
|
115
122
|
|
|
116
123
|
|
|
124
|
+
class ConductorConfig(BaseModel):
|
|
125
|
+
"""
|
|
126
|
+
Configuration for the Conductor orchestration system.
|
|
127
|
+
|
|
128
|
+
Controls token budget management and agent spawning throttling.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
daily_budget_usd: float = Field(
|
|
132
|
+
default=50.0,
|
|
133
|
+
ge=0.0,
|
|
134
|
+
description="Daily budget limit in USD. Set to 0 for unlimited.",
|
|
135
|
+
)
|
|
136
|
+
warning_threshold: float = Field(
|
|
137
|
+
default=0.8,
|
|
138
|
+
ge=0.0,
|
|
139
|
+
le=1.0,
|
|
140
|
+
description="Budget percentage at which to issue warnings (0.0-1.0).",
|
|
141
|
+
)
|
|
142
|
+
throttle_threshold: float = Field(
|
|
143
|
+
default=0.9,
|
|
144
|
+
ge=0.0,
|
|
145
|
+
le=1.0,
|
|
146
|
+
description="Budget percentage at which to throttle agent spawning (0.0-1.0).",
|
|
147
|
+
)
|
|
148
|
+
tracking_window_days: int = Field(
|
|
149
|
+
default=7,
|
|
150
|
+
gt=0,
|
|
151
|
+
description="Number of days to track usage for reporting.",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
117
155
|
def expand_env_vars(content: str) -> str:
|
|
118
156
|
"""
|
|
119
157
|
Expand environment variables in configuration content.
|
|
@@ -188,13 +226,17 @@ class DaemonConfig(BaseModel):
|
|
|
188
226
|
|
|
189
227
|
# Daemon settings
|
|
190
228
|
daemon_port: int = Field(
|
|
191
|
-
default=
|
|
229
|
+
default=60887,
|
|
192
230
|
description="Port for daemon to listen on",
|
|
193
231
|
)
|
|
194
232
|
daemon_health_check_interval: float = Field(
|
|
195
233
|
default=10.0,
|
|
196
234
|
description="Daemon health check interval in seconds",
|
|
197
235
|
)
|
|
236
|
+
test_mode: bool = Field(
|
|
237
|
+
default=False,
|
|
238
|
+
description="Run daemon in test mode (enables test endpoints)",
|
|
239
|
+
)
|
|
198
240
|
|
|
199
241
|
# Local storage
|
|
200
242
|
database_path: str = Field(
|
|
@@ -279,6 +321,10 @@ class DaemonConfig(BaseModel):
|
|
|
279
321
|
default_factory=MemorySyncConfig,
|
|
280
322
|
description="Memory synchronization configuration",
|
|
281
323
|
)
|
|
324
|
+
skills: SkillsConfig = Field(
|
|
325
|
+
default_factory=SkillsConfig,
|
|
326
|
+
description="Skills injection configuration",
|
|
327
|
+
)
|
|
282
328
|
message_tracking: MessageTrackingConfig = Field(
|
|
283
329
|
default_factory=MessageTrackingConfig,
|
|
284
330
|
description="Session message tracking configuration",
|
|
@@ -295,6 +341,14 @@ class DaemonConfig(BaseModel):
|
|
|
295
341
|
default_factory=ProjectVerificationConfig,
|
|
296
342
|
description="Default verification commands for projects without auto-detected config",
|
|
297
343
|
)
|
|
344
|
+
conductor: ConductorConfig = Field(
|
|
345
|
+
default_factory=ConductorConfig,
|
|
346
|
+
description="Conductor orchestration system configuration",
|
|
347
|
+
)
|
|
348
|
+
search: SearchConfig = Field(
|
|
349
|
+
default_factory=SearchConfig,
|
|
350
|
+
description="Unified search configuration with embedding fallback",
|
|
351
|
+
)
|
|
298
352
|
|
|
299
353
|
def get_recommend_tools_config(self) -> RecommendToolsConfig:
|
|
300
354
|
"""Get recommend_tools configuration."""
|
|
@@ -324,6 +378,10 @@ class DaemonConfig(BaseModel):
|
|
|
324
378
|
"""Get memory sync configuration."""
|
|
325
379
|
return self.memory_sync
|
|
326
380
|
|
|
381
|
+
def get_skills_config(self) -> SkillsConfig:
|
|
382
|
+
"""Get skills configuration."""
|
|
383
|
+
return self.skills
|
|
384
|
+
|
|
327
385
|
def get_gobby_tasks_config(self) -> GobbyTasksConfig:
|
|
328
386
|
"""Get gobby-tasks configuration."""
|
|
329
387
|
return self.gobby_tasks
|
|
@@ -336,6 +394,10 @@ class DaemonConfig(BaseModel):
|
|
|
336
394
|
"""Get default verification commands configuration."""
|
|
337
395
|
return self.verification_defaults
|
|
338
396
|
|
|
397
|
+
def get_search_config(self) -> SearchConfig:
|
|
398
|
+
"""Get search configuration."""
|
|
399
|
+
return self.search
|
|
400
|
+
|
|
339
401
|
@field_validator("daemon_port")
|
|
340
402
|
@classmethod
|
|
341
403
|
def validate_port(cls, v: int) -> int:
|
gobby/config/search.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Search configuration for Gobby daemon.
|
|
2
|
+
|
|
3
|
+
Provides configuration for the unified search layer with embedding
|
|
4
|
+
support and TF-IDF fallback.
|
|
5
|
+
|
|
6
|
+
Example usage in config.yaml:
|
|
7
|
+
search:
|
|
8
|
+
mode: auto # tfidf, embedding, auto, hybrid
|
|
9
|
+
embedding_model: text-embedding-3-small
|
|
10
|
+
tfidf_weight: 0.4
|
|
11
|
+
embedding_weight: 0.6
|
|
12
|
+
notify_on_fallback: true
|
|
13
|
+
|
|
14
|
+
For Ollama (local embeddings):
|
|
15
|
+
search:
|
|
16
|
+
mode: auto
|
|
17
|
+
embedding_model: openai/nomic-embed-text
|
|
18
|
+
embedding_api_base: http://localhost:11434/v1
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
|
|
23
|
+
from gobby.search.models import SearchMode
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SearchConfig(BaseModel):
|
|
27
|
+
"""Configuration for unified search with fallback.
|
|
28
|
+
|
|
29
|
+
This config controls how UnifiedSearcher behaves, including:
|
|
30
|
+
- Which search mode to use (tfidf, embedding, auto, hybrid)
|
|
31
|
+
- Which embedding model to use (LiteLLM format)
|
|
32
|
+
- Weights for hybrid mode
|
|
33
|
+
- Whether to notify on fallback
|
|
34
|
+
|
|
35
|
+
Supported modes:
|
|
36
|
+
- tfidf: TF-IDF only (always works, no API needed)
|
|
37
|
+
- embedding: Embedding-based search only (fails if unavailable)
|
|
38
|
+
- auto: Try embedding, fallback to TF-IDF if unavailable
|
|
39
|
+
- hybrid: Combine both with weighted scores
|
|
40
|
+
|
|
41
|
+
LiteLLM model format examples:
|
|
42
|
+
- OpenAI: text-embedding-3-small (needs OPENAI_API_KEY)
|
|
43
|
+
- Ollama: openai/nomic-embed-text (with embedding_api_base)
|
|
44
|
+
- Azure: azure/azure-embedding-model
|
|
45
|
+
- Vertex AI: vertex_ai/text-embedding-004
|
|
46
|
+
- Gemini: gemini/text-embedding-004 (needs GEMINI_API_KEY)
|
|
47
|
+
- Mistral: mistral/mistral-embed (needs MISTRAL_API_KEY)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
mode: str = Field(
|
|
51
|
+
default="auto",
|
|
52
|
+
description="Search mode: tfidf, embedding, auto, hybrid",
|
|
53
|
+
)
|
|
54
|
+
embedding_model: str = Field(
|
|
55
|
+
default="text-embedding-3-small",
|
|
56
|
+
description="LiteLLM model string (e.g., text-embedding-3-small, openai/nomic-embed-text)",
|
|
57
|
+
)
|
|
58
|
+
embedding_api_base: str | None = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
description="API base URL for Ollama/custom endpoints (e.g., http://localhost:11434/v1)",
|
|
61
|
+
)
|
|
62
|
+
embedding_api_key: str | None = Field(
|
|
63
|
+
default=None,
|
|
64
|
+
description="API key for embedding provider (uses env var if not set)",
|
|
65
|
+
)
|
|
66
|
+
tfidf_weight: float = Field(
|
|
67
|
+
default=0.4,
|
|
68
|
+
ge=0.0,
|
|
69
|
+
le=1.0,
|
|
70
|
+
description="Weight for TF-IDF scores in hybrid mode",
|
|
71
|
+
)
|
|
72
|
+
embedding_weight: float = Field(
|
|
73
|
+
default=0.6,
|
|
74
|
+
ge=0.0,
|
|
75
|
+
le=1.0,
|
|
76
|
+
description="Weight for embedding scores in hybrid mode",
|
|
77
|
+
)
|
|
78
|
+
notify_on_fallback: bool = Field(
|
|
79
|
+
default=True,
|
|
80
|
+
description="Log warning when falling back to TF-IDF",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def get_normalized_weights(self) -> tuple[float, float]:
|
|
84
|
+
"""Get normalized weights that sum to 1.0.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Tuple of (tfidf_weight, embedding_weight) normalized to sum to 1.0
|
|
88
|
+
"""
|
|
89
|
+
total = self.tfidf_weight + self.embedding_weight
|
|
90
|
+
if total == 0:
|
|
91
|
+
# Default to equal weights if both are 0
|
|
92
|
+
return (0.5, 0.5)
|
|
93
|
+
return (self.tfidf_weight / total, self.embedding_weight / total)
|
|
94
|
+
|
|
95
|
+
def get_mode_enum(self) -> SearchMode:
|
|
96
|
+
"""Get the SearchMode enum instance for the configured mode.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
SearchMode enum corresponding to the mode string value
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValueError: If the configured mode is not a valid SearchMode
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
return SearchMode(self.mode)
|
|
106
|
+
except ValueError as e:
|
|
107
|
+
valid_modes = [m.value for m in SearchMode]
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Invalid search mode '{self.mode}'. Valid modes are: {', '.join(valid_modes)}"
|
|
110
|
+
) from e
|
gobby/config/servers.py
CHANGED
|
@@ -23,7 +23,7 @@ class WebSocketSettings(BaseModel):
|
|
|
23
23
|
description="Enable WebSocket server for real-time communication",
|
|
24
24
|
)
|
|
25
25
|
port: int = Field(
|
|
26
|
-
default=
|
|
26
|
+
default=60888,
|
|
27
27
|
description="Port for WebSocket server to listen on",
|
|
28
28
|
)
|
|
29
29
|
ping_interval: int = Field(
|
gobby/config/skills.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skills configuration for Gobby daemon.
|
|
3
|
+
|
|
4
|
+
Provides configuration for skill injection and discovery.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SkillsConfig(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Configuration for skill injection and discovery.
|
|
17
|
+
|
|
18
|
+
Controls whether and how skills are injected into session context.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
inject_core_skills: bool = Field(
|
|
22
|
+
default=True,
|
|
23
|
+
description="Whether to inject core skills into session context",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
core_skills_path: str | None = Field(
|
|
27
|
+
default=None,
|
|
28
|
+
description="Override path for core skills (default: install/shared/skills/)",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
injection_format: Literal["summary", "full", "none"] = Field(
|
|
32
|
+
default="summary",
|
|
33
|
+
description="Format for skill injection: 'summary' (names only), 'full' (with content), 'none' (disabled)",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@field_validator("injection_format")
|
|
37
|
+
@classmethod
|
|
38
|
+
def validate_injection_format(cls, v: str) -> str:
|
|
39
|
+
"""Validate injection_format is one of the allowed values."""
|
|
40
|
+
allowed = {"summary", "full", "none"}
|
|
41
|
+
if v not in allowed:
|
|
42
|
+
raise ValueError(f"injection_format must be one of {allowed}, got '{v}'")
|
|
43
|
+
return v
|
gobby/config/tasks.py
CHANGED
|
@@ -181,16 +181,12 @@ class TaskExpansionConfig(BaseModel):
|
|
|
181
181
|
)
|
|
182
182
|
tdd_prompt: str | None = Field(
|
|
183
183
|
default=None,
|
|
184
|
-
description="DEPRECATED: TDD
|
|
184
|
+
description="DEPRECATED: TDD instructions are now embedded in task descriptions for code/config categories",
|
|
185
185
|
)
|
|
186
186
|
web_research_enabled: bool = Field(
|
|
187
187
|
default=True,
|
|
188
188
|
description="Enable web research for task expansion using MCP tools",
|
|
189
189
|
)
|
|
190
|
-
tdd_mode: bool = Field(
|
|
191
|
-
default=True,
|
|
192
|
-
description="Enable TDD mode: create test->implement task pairs with appropriate blocking for coding tasks",
|
|
193
|
-
)
|
|
194
190
|
max_subtasks: int = Field(
|
|
195
191
|
default=15,
|
|
196
192
|
description="Maximum number of subtasks to create per expansion",
|
|
@@ -724,10 +720,6 @@ class WorkflowVariablesConfig(BaseModel):
|
|
|
724
720
|
default=False,
|
|
725
721
|
description="Require an active task (in_progress) before allowing file edits",
|
|
726
722
|
)
|
|
727
|
-
tdd_mode: bool = Field(
|
|
728
|
-
default=True,
|
|
729
|
-
description="Enable TDD mode for task expansion (test-implementation pairs)",
|
|
730
|
-
)
|
|
731
723
|
session_task: str | list[str] | None = Field(
|
|
732
724
|
default=None,
|
|
733
725
|
description="Task(s) to complete before stopping. "
|
|
@@ -759,13 +751,13 @@ def merge_workflow_variables(
|
|
|
759
751
|
ValidationError: If validate=True and merged values fail validation.
|
|
760
752
|
|
|
761
753
|
Example:
|
|
762
|
-
>>> yaml_defaults = {"
|
|
763
|
-
>>> db_overrides = {"
|
|
754
|
+
>>> yaml_defaults = {"require_task_before_edit": False, "session_task": None}
|
|
755
|
+
>>> db_overrides = {"require_task_before_edit": True}
|
|
764
756
|
>>> effective = merge_workflow_variables(yaml_defaults, db_overrides)
|
|
765
|
-
>>> effective["tdd_mode"]
|
|
766
|
-
False
|
|
767
757
|
>>> effective["require_task_before_edit"]
|
|
768
|
-
|
|
758
|
+
True
|
|
759
|
+
>>> effective["session_task"]
|
|
760
|
+
None
|
|
769
761
|
"""
|
|
770
762
|
# Start with defaults
|
|
771
763
|
effective = dict(yaml_defaults)
|