gobby 0.2.7__py3-none-any.whl → 0.2.9__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/claude_code.py +99 -61
- gobby/adapters/gemini.py +140 -38
- gobby/agents/isolation.py +130 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn_executor.py +43 -13
- gobby/agents/spawners/macos.py +26 -1
- gobby/app_context.py +59 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/memory.py +185 -0
- gobby/cli/utils.py +5 -17
- gobby/clones/git.py +177 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +87 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +573 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/hook_manager.py +21 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/claude.py +377 -42
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +2 -2
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/registries.py +35 -4
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +45 -9
- gobby/mcp_proxy/tools/artifacts.py +46 -12
- gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
- gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
- gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
- gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
- gobby/mcp_proxy/tools/spawn_agent.py +44 -6
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows/__init__.py +266 -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 +321 -0
- gobby/mcp_proxy/tools/workflows/_query.py +207 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
- gobby/mcp_proxy/tools/worktrees.py +32 -7
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/extractor.py +15 -1
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +36 -10
- gobby/servers/http.py +186 -149
- gobby/servers/routes/admin.py +12 -0
- gobby/servers/routes/mcp/endpoints/execution.py +15 -7
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +50 -3
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +4 -4
- gobby/sessions/manager.py +9 -0
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +46 -4
- gobby/storage/sessions.py +4 -2
- gobby/storage/skills.py +87 -7
- 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 +5 -0
- gobby/workflows/context_actions.py +21 -24
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +109 -1
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/engine.py +96 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/lifecycle_evaluator.py +2 -1
- gobby/workflows/memory_actions.py +11 -0
- gobby/workflows/safe_evaluator.py +8 -0
- gobby/workflows/summary_actions.py +123 -50
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/RECORD +99 -107
- gobby/cli/tui.py +0 -34
- gobby/hooks/event_handlers.py +0 -909
- gobby/mcp_proxy/tools/workflows.py +0 -973
- gobby/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/top_level.txt +0 -0
gobby/mcp_proxy/registries.py
CHANGED
|
@@ -143,6 +143,7 @@ def setup_internal_registries(
|
|
|
143
143
|
|
|
144
144
|
workflows_registry = create_workflows_registry(
|
|
145
145
|
session_manager=local_session_manager,
|
|
146
|
+
db=getattr(local_session_manager, "db", None) if local_session_manager else None,
|
|
146
147
|
)
|
|
147
148
|
manager.add_registry(workflows_registry)
|
|
148
149
|
logger.debug("Workflows registry initialized")
|
|
@@ -168,21 +169,38 @@ def setup_internal_registries(
|
|
|
168
169
|
|
|
169
170
|
# Initialize agents registry if agent_runner is available
|
|
170
171
|
if agent_runner is not None:
|
|
171
|
-
from gobby.agents.
|
|
172
|
+
from gobby.agents.definitions import AgentDefinitionLoader
|
|
172
173
|
from gobby.mcp_proxy.tools.agents import create_agents_registry
|
|
173
174
|
|
|
175
|
+
# Create clone git manager if we have a git manager
|
|
176
|
+
clone_git_manager = None
|
|
177
|
+
if git_manager is not None:
|
|
178
|
+
try:
|
|
179
|
+
from gobby.clones.git import CloneGitManager
|
|
180
|
+
|
|
181
|
+
clone_git_manager = CloneGitManager(git_manager.repo_path)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.debug(f"CloneGitManager not available for spawn_agent: {e}")
|
|
184
|
+
|
|
174
185
|
agents_registry = create_agents_registry(
|
|
175
186
|
runner=agent_runner,
|
|
187
|
+
agent_loader=AgentDefinitionLoader(),
|
|
188
|
+
session_manager=local_session_manager,
|
|
189
|
+
task_manager=task_manager,
|
|
190
|
+
worktree_storage=worktree_storage,
|
|
191
|
+
git_manager=git_manager,
|
|
192
|
+
clone_storage=clone_storage,
|
|
193
|
+
clone_manager=clone_git_manager,
|
|
176
194
|
)
|
|
177
195
|
|
|
178
|
-
# Add inter-agent messaging tools if message manager
|
|
179
|
-
if inter_session_message_manager is not None:
|
|
196
|
+
# Add inter-agent messaging tools if message manager and session manager are available
|
|
197
|
+
if inter_session_message_manager is not None and local_session_manager is not None:
|
|
180
198
|
from gobby.mcp_proxy.tools.agent_messaging import add_messaging_tools
|
|
181
199
|
|
|
182
200
|
add_messaging_tools(
|
|
183
201
|
registry=agents_registry,
|
|
184
202
|
message_manager=inter_session_message_manager,
|
|
185
|
-
|
|
203
|
+
session_manager=local_session_manager,
|
|
186
204
|
)
|
|
187
205
|
logger.debug("Agent messaging tools added to agents registry")
|
|
188
206
|
|
|
@@ -264,6 +282,19 @@ def setup_internal_registries(
|
|
|
264
282
|
else:
|
|
265
283
|
logger.debug("Skills registry not initialized: task_manager is None")
|
|
266
284
|
|
|
285
|
+
# Initialize artifacts registry using the existing database from task_manager
|
|
286
|
+
if task_manager is not None:
|
|
287
|
+
from gobby.mcp_proxy.tools.artifacts import create_artifacts_registry
|
|
288
|
+
|
|
289
|
+
artifacts_registry = create_artifacts_registry(
|
|
290
|
+
db=task_manager.db,
|
|
291
|
+
session_manager=local_session_manager,
|
|
292
|
+
)
|
|
293
|
+
manager.add_registry(artifacts_registry)
|
|
294
|
+
logger.debug("Artifacts registry initialized")
|
|
295
|
+
else:
|
|
296
|
+
logger.debug("Artifacts registry not initialized: task_manager is None")
|
|
297
|
+
|
|
267
298
|
logger.info(f"Internal registries initialized: {len(manager)} registries")
|
|
268
299
|
return manager
|
|
269
300
|
|
|
@@ -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)
|
|
@@ -6,9 +6,10 @@ Provides messaging capabilities between parent and child sessions:
|
|
|
6
6
|
- send_to_child: Parent sends message to a specific child
|
|
7
7
|
- poll_messages: Check for incoming messages
|
|
8
8
|
- mark_message_read: Mark a message as read
|
|
9
|
-
- broadcast_to_children: Send message to all
|
|
9
|
+
- broadcast_to_children: Send message to all children (active in database)
|
|
10
10
|
|
|
11
|
-
These tools resolve session relationships from
|
|
11
|
+
These tools resolve session relationships from the database (LocalSessionManager),
|
|
12
|
+
which is the authoritative source for parent_session_id relationships.
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
15
|
from __future__ import annotations
|
|
@@ -17,9 +18,9 @@ import logging
|
|
|
17
18
|
from typing import TYPE_CHECKING, Any
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
|
-
from gobby.agents.registry import RunningAgentRegistry
|
|
21
21
|
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
22
22
|
from gobby.storage.inter_session_messages import InterSessionMessageManager
|
|
23
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
25
26
|
|
|
@@ -27,7 +28,7 @@ logger = logging.getLogger(__name__)
|
|
|
27
28
|
def add_messaging_tools(
|
|
28
29
|
registry: InternalToolRegistry,
|
|
29
30
|
message_manager: InterSessionMessageManager,
|
|
30
|
-
|
|
31
|
+
session_manager: LocalSessionManager,
|
|
31
32
|
) -> None:
|
|
32
33
|
"""
|
|
33
34
|
Add inter-agent messaging tools to an existing registry.
|
|
@@ -35,12 +36,20 @@ def add_messaging_tools(
|
|
|
35
36
|
Args:
|
|
36
37
|
registry: The InternalToolRegistry to add tools to (typically gobby-agents)
|
|
37
38
|
message_manager: InterSessionMessageManager for persisting messages
|
|
38
|
-
|
|
39
|
+
session_manager: LocalSessionManager for resolving parent/child relationships
|
|
40
|
+
(database is the authoritative source for session relationships)
|
|
39
41
|
"""
|
|
42
|
+
from gobby.utils.project_context import get_project_context
|
|
43
|
+
|
|
44
|
+
def _resolve_session_id(ref: str) -> str:
|
|
45
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID."""
|
|
46
|
+
project_ctx = get_project_context()
|
|
47
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
48
|
+
return session_manager.resolve_session_reference(ref, project_id)
|
|
40
49
|
|
|
41
50
|
@registry.tool(
|
|
42
51
|
name="send_to_parent",
|
|
43
|
-
description="Send a message from a child session to its parent session.",
|
|
52
|
+
description="Send a message from a child session to its parent session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
44
53
|
)
|
|
45
54
|
async def send_to_parent(
|
|
46
55
|
session_id: str,
|
|
@@ -54,7 +63,7 @@ def add_messaging_tools(
|
|
|
54
63
|
or requests back to its parent session.
|
|
55
64
|
|
|
56
65
|
Args:
|
|
57
|
-
session_id:
|
|
66
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) for the current (child) session
|
|
58
67
|
content: Message content to send
|
|
59
68
|
priority: Message priority ("normal" or "urgent")
|
|
60
69
|
|
|
@@ -62,30 +71,41 @@ def add_messaging_tools(
|
|
|
62
71
|
Dict with success status and message details
|
|
63
72
|
"""
|
|
64
73
|
try:
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
75
|
+
try:
|
|
76
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
return {"success": False, "error": str(e)}
|
|
79
|
+
|
|
80
|
+
# Look up session in database (authoritative source for relationships)
|
|
81
|
+
session = session_manager.get(resolved_session_id)
|
|
82
|
+
if not session:
|
|
68
83
|
return {
|
|
69
84
|
"success": False,
|
|
70
|
-
"error": f"Session {
|
|
85
|
+
"error": f"Session {resolved_session_id} not found",
|
|
71
86
|
}
|
|
72
87
|
|
|
73
|
-
parent_session_id =
|
|
88
|
+
parent_session_id = session.parent_session_id
|
|
74
89
|
if not parent_session_id:
|
|
75
90
|
return {
|
|
76
91
|
"success": False,
|
|
77
|
-
"error": "No parent session
|
|
92
|
+
"error": "No parent session for this session",
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
# Create the message
|
|
81
96
|
msg = message_manager.create_message(
|
|
82
|
-
from_session=
|
|
97
|
+
from_session=resolved_session_id,
|
|
83
98
|
to_session=parent_session_id,
|
|
84
99
|
content=content,
|
|
85
100
|
priority=priority,
|
|
86
101
|
)
|
|
87
102
|
|
|
88
|
-
logger.info(
|
|
103
|
+
logger.info(
|
|
104
|
+
"Message sent from %s to parent %s: %s",
|
|
105
|
+
resolved_session_id,
|
|
106
|
+
parent_session_id,
|
|
107
|
+
msg.id,
|
|
108
|
+
)
|
|
89
109
|
|
|
90
110
|
return {
|
|
91
111
|
"success": True,
|
|
@@ -94,7 +114,7 @@ def add_messaging_tools(
|
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
except Exception as e:
|
|
97
|
-
logger.error(
|
|
117
|
+
logger.error("Failed to send message to parent: %s", e)
|
|
98
118
|
return {
|
|
99
119
|
"success": False,
|
|
100
120
|
"error": str(e),
|
|
@@ -102,7 +122,7 @@ def add_messaging_tools(
|
|
|
102
122
|
|
|
103
123
|
@registry.tool(
|
|
104
124
|
name="send_to_child",
|
|
105
|
-
description="Send a message from a parent session to a specific child session.",
|
|
125
|
+
description="Send a message from a parent session to a specific child session. Accepts #N, N, UUID, or prefix for session IDs.",
|
|
106
126
|
)
|
|
107
127
|
async def send_to_child(
|
|
108
128
|
parent_session_id: str,
|
|
@@ -117,8 +137,8 @@ def add_messaging_tools(
|
|
|
117
137
|
updates, or coordination messages to a spawned child.
|
|
118
138
|
|
|
119
139
|
Args:
|
|
120
|
-
parent_session_id:
|
|
121
|
-
child_session_id:
|
|
140
|
+
parent_session_id: Session reference (accepts #N, N, UUID, or prefix) for the parent (sender)
|
|
141
|
+
child_session_id: Session reference (accepts #N, N, UUID, or prefix) for the child (recipient)
|
|
122
142
|
content: Message content to send
|
|
123
143
|
priority: Message priority ("normal" or "urgent")
|
|
124
144
|
|
|
@@ -126,33 +146,43 @@ def add_messaging_tools(
|
|
|
126
146
|
Dict with success status and message details
|
|
127
147
|
"""
|
|
128
148
|
try:
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
149
|
+
# Resolve session IDs to UUIDs (accepts #N, N, UUID, or prefix)
|
|
150
|
+
try:
|
|
151
|
+
resolved_parent_id = _resolve_session_id(parent_session_id)
|
|
152
|
+
resolved_child_id = _resolve_session_id(child_session_id)
|
|
153
|
+
except ValueError as e:
|
|
154
|
+
return {"success": False, "error": str(e)}
|
|
155
|
+
|
|
156
|
+
# Verify the child exists in database and belongs to this parent
|
|
157
|
+
child_session = session_manager.get(resolved_child_id)
|
|
158
|
+
if not child_session:
|
|
132
159
|
return {
|
|
133
160
|
"success": False,
|
|
134
|
-
"error": f"Child session {
|
|
161
|
+
"error": f"Child session {resolved_child_id} not found",
|
|
135
162
|
}
|
|
136
163
|
|
|
137
|
-
if
|
|
164
|
+
if child_session.parent_session_id != resolved_parent_id:
|
|
138
165
|
return {
|
|
139
166
|
"success": False,
|
|
140
167
|
"error": (
|
|
141
|
-
f"Session {
|
|
142
|
-
f"Actual parent: {
|
|
168
|
+
f"Session {resolved_child_id} is not a child of {resolved_parent_id}. "
|
|
169
|
+
f"Actual parent: {child_session.parent_session_id}"
|
|
143
170
|
),
|
|
144
171
|
}
|
|
145
172
|
|
|
146
173
|
# Create the message
|
|
147
174
|
msg = message_manager.create_message(
|
|
148
|
-
from_session=
|
|
149
|
-
to_session=
|
|
175
|
+
from_session=resolved_parent_id,
|
|
176
|
+
to_session=resolved_child_id,
|
|
150
177
|
content=content,
|
|
151
178
|
priority=priority,
|
|
152
179
|
)
|
|
153
180
|
|
|
154
181
|
logger.info(
|
|
155
|
-
|
|
182
|
+
"Message sent from %s to child %s: %s",
|
|
183
|
+
resolved_parent_id,
|
|
184
|
+
resolved_child_id,
|
|
185
|
+
msg.id,
|
|
156
186
|
)
|
|
157
187
|
|
|
158
188
|
return {
|
|
@@ -161,7 +191,7 @@ def add_messaging_tools(
|
|
|
161
191
|
}
|
|
162
192
|
|
|
163
193
|
except Exception as e:
|
|
164
|
-
logger.error(
|
|
194
|
+
logger.error("Failed to send message to child: %s", e)
|
|
165
195
|
return {
|
|
166
196
|
"success": False,
|
|
167
197
|
"error": str(e),
|
|
@@ -169,7 +199,7 @@ def add_messaging_tools(
|
|
|
169
199
|
|
|
170
200
|
@registry.tool(
|
|
171
201
|
name="poll_messages",
|
|
172
|
-
description="Poll for messages sent to this session.",
|
|
202
|
+
description="Poll for messages sent to this session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
173
203
|
)
|
|
174
204
|
async def poll_messages(
|
|
175
205
|
session_id: str,
|
|
@@ -182,15 +212,21 @@ def add_messaging_tools(
|
|
|
182
212
|
By default, returns only unread messages.
|
|
183
213
|
|
|
184
214
|
Args:
|
|
185
|
-
session_id:
|
|
215
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) to check messages for
|
|
186
216
|
unread_only: If True, only return unread messages (default: True)
|
|
187
217
|
|
|
188
218
|
Returns:
|
|
189
219
|
Dict with success status and list of messages
|
|
190
220
|
"""
|
|
191
221
|
try:
|
|
222
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
223
|
+
try:
|
|
224
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
225
|
+
except ValueError as e:
|
|
226
|
+
return {"success": False, "error": str(e)}
|
|
227
|
+
|
|
192
228
|
messages = message_manager.get_messages(
|
|
193
|
-
to_session=
|
|
229
|
+
to_session=resolved_session_id,
|
|
194
230
|
unread_only=unread_only,
|
|
195
231
|
)
|
|
196
232
|
|
|
@@ -248,7 +284,7 @@ def add_messaging_tools(
|
|
|
248
284
|
|
|
249
285
|
@registry.tool(
|
|
250
286
|
name="broadcast_to_children",
|
|
251
|
-
description="Broadcast a message to all
|
|
287
|
+
description="Broadcast a message to all active child sessions. Accepts #N, N, UUID, or prefix for session_id.",
|
|
252
288
|
)
|
|
253
289
|
async def broadcast_to_children(
|
|
254
290
|
parent_session_id: str,
|
|
@@ -256,13 +292,14 @@ def add_messaging_tools(
|
|
|
256
292
|
priority: str = "normal",
|
|
257
293
|
) -> dict[str, Any]:
|
|
258
294
|
"""
|
|
259
|
-
Broadcast a message to all
|
|
295
|
+
Broadcast a message to all active children.
|
|
260
296
|
|
|
261
|
-
Send the same message to all child sessions spawned by this parent
|
|
297
|
+
Send the same message to all child sessions spawned by this parent
|
|
298
|
+
that are currently active in the database.
|
|
262
299
|
Useful for coordination or shutdown signals.
|
|
263
300
|
|
|
264
301
|
Args:
|
|
265
|
-
parent_session_id:
|
|
302
|
+
parent_session_id: Session reference (accepts #N, N, UUID, or prefix) for the parent
|
|
266
303
|
content: Message content to broadcast
|
|
267
304
|
priority: Message priority ("normal" or "urgent")
|
|
268
305
|
|
|
@@ -270,13 +307,22 @@ def add_messaging_tools(
|
|
|
270
307
|
Dict with success status and count of messages sent
|
|
271
308
|
"""
|
|
272
309
|
try:
|
|
273
|
-
|
|
310
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
311
|
+
try:
|
|
312
|
+
resolved_parent_id = _resolve_session_id(parent_session_id)
|
|
313
|
+
except ValueError as e:
|
|
314
|
+
return {"success": False, "error": str(e)}
|
|
315
|
+
|
|
316
|
+
# Get all children from database
|
|
317
|
+
all_children = session_manager.find_children(resolved_parent_id)
|
|
318
|
+
# Filter to active children only
|
|
319
|
+
children = [c for c in all_children if c.status == "active"]
|
|
274
320
|
|
|
275
321
|
if not children:
|
|
276
322
|
return {
|
|
277
323
|
"success": True,
|
|
278
324
|
"sent_count": 0,
|
|
279
|
-
"message": "No
|
|
325
|
+
"message": "No active children found",
|
|
280
326
|
}
|
|
281
327
|
|
|
282
328
|
sent_count = 0
|
|
@@ -285,14 +331,14 @@ def add_messaging_tools(
|
|
|
285
331
|
for child in children:
|
|
286
332
|
try:
|
|
287
333
|
message_manager.create_message(
|
|
288
|
-
from_session=
|
|
289
|
-
to_session=child.
|
|
334
|
+
from_session=resolved_parent_id,
|
|
335
|
+
to_session=child.id,
|
|
290
336
|
content=content,
|
|
291
337
|
priority=priority,
|
|
292
338
|
)
|
|
293
339
|
sent_count += 1
|
|
294
340
|
except Exception as e:
|
|
295
|
-
errors.append(f"{child.
|
|
341
|
+
errors.append(f"{child.id}: {e}")
|
|
296
342
|
|
|
297
343
|
result: dict[str, Any] = {
|
|
298
344
|
"success": True,
|
|
@@ -304,13 +350,16 @@ def add_messaging_tools(
|
|
|
304
350
|
result["errors"] = errors
|
|
305
351
|
|
|
306
352
|
logger.info(
|
|
307
|
-
|
|
353
|
+
"Broadcast from %s sent to %d/%d children",
|
|
354
|
+
resolved_parent_id,
|
|
355
|
+
sent_count,
|
|
356
|
+
len(children),
|
|
308
357
|
)
|
|
309
358
|
|
|
310
359
|
return result
|
|
311
360
|
|
|
312
361
|
except Exception as e:
|
|
313
|
-
logger.error(
|
|
362
|
+
logger.error("Failed to broadcast to children: %s", e)
|
|
314
363
|
return {
|
|
315
364
|
"success": False,
|
|
316
365
|
"error": str(e),
|
gobby/mcp_proxy/tools/agents.py
CHANGED
|
@@ -32,6 +32,7 @@ def create_agents_registry(
|
|
|
32
32
|
runner: AgentRunner,
|
|
33
33
|
running_registry: RunningAgentRegistry | None = None,
|
|
34
34
|
workflow_state_manager: Any | None = None,
|
|
35
|
+
session_manager: Any | None = None,
|
|
35
36
|
# spawn_agent dependencies
|
|
36
37
|
agent_loader: Any | None = None,
|
|
37
38
|
task_manager: Any | None = None,
|
|
@@ -48,6 +49,7 @@ def create_agents_registry(
|
|
|
48
49
|
running_registry: Optional in-memory registry for running agents.
|
|
49
50
|
workflow_state_manager: Optional WorkflowStateManager for stopping workflows
|
|
50
51
|
when agents are killed. If not provided, workflow stop will be skipped.
|
|
52
|
+
session_manager: Optional LocalSessionManager for resolving session references.
|
|
51
53
|
agent_loader: Agent definition loader for spawn_agent.
|
|
52
54
|
task_manager: Task manager for spawn_agent task resolution.
|
|
53
55
|
worktree_storage: Worktree storage for spawn_agent isolation.
|
|
@@ -58,6 +60,16 @@ def create_agents_registry(
|
|
|
58
60
|
Returns:
|
|
59
61
|
InternalToolRegistry with all agent tools registered.
|
|
60
62
|
"""
|
|
63
|
+
from gobby.utils.project_context import get_project_context
|
|
64
|
+
|
|
65
|
+
def _resolve_session_id(ref: str) -> str:
|
|
66
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID."""
|
|
67
|
+
if session_manager is None:
|
|
68
|
+
return ref # No resolution available, return as-is
|
|
69
|
+
project_ctx = get_project_context()
|
|
70
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
71
|
+
return str(session_manager.resolve_session_reference(ref, project_id))
|
|
72
|
+
|
|
61
73
|
registry = InternalToolRegistry(
|
|
62
74
|
name="gobby-agents",
|
|
63
75
|
description="Agent spawning - start, monitor, and manage subagents",
|
|
@@ -105,7 +117,7 @@ def create_agents_registry(
|
|
|
105
117
|
|
|
106
118
|
@registry.tool(
|
|
107
119
|
name="list_agents",
|
|
108
|
-
description="List agent runs for a session.",
|
|
120
|
+
description="List agent runs for a session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
109
121
|
)
|
|
110
122
|
async def list_agents(
|
|
111
123
|
parent_session_id: str,
|
|
@@ -116,14 +128,20 @@ def create_agents_registry(
|
|
|
116
128
|
List agent runs for a session.
|
|
117
129
|
|
|
118
130
|
Args:
|
|
119
|
-
parent_session_id:
|
|
131
|
+
parent_session_id: Session reference (accepts #N, N, UUID, or prefix) for the parent.
|
|
120
132
|
status: Optional status filter (pending, running, success, error, timeout, cancelled).
|
|
121
133
|
limit: Maximum results (default: 20).
|
|
122
134
|
|
|
123
135
|
Returns:
|
|
124
136
|
Dict with list of agent runs.
|
|
125
137
|
"""
|
|
126
|
-
|
|
138
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
139
|
+
try:
|
|
140
|
+
resolved_parent_id = _resolve_session_id(parent_session_id)
|
|
141
|
+
except ValueError as e:
|
|
142
|
+
return {"success": False, "error": str(e)}
|
|
143
|
+
|
|
144
|
+
runs = runner.list_runs(resolved_parent_id, status=status, limit=limit)
|
|
127
145
|
|
|
128
146
|
return {
|
|
129
147
|
"success": True,
|
|
@@ -221,6 +239,12 @@ def create_agents_registry(
|
|
|
221
239
|
agent = agent_registry.get(run_id)
|
|
222
240
|
session_id = agent.session_id if agent else None
|
|
223
241
|
|
|
242
|
+
# Database fallback: if not in registry, look up from DB
|
|
243
|
+
if session_id is None:
|
|
244
|
+
db_run = runner.get_run(run_id)
|
|
245
|
+
if db_run and db_run.child_session_id:
|
|
246
|
+
session_id = db_run.child_session_id
|
|
247
|
+
|
|
224
248
|
# Kill via registry (run in thread to avoid blocking event loop)
|
|
225
249
|
import asyncio
|
|
226
250
|
|
|
@@ -245,7 +269,7 @@ def create_agents_registry(
|
|
|
245
269
|
|
|
246
270
|
@registry.tool(
|
|
247
271
|
name="can_spawn_agent",
|
|
248
|
-
description="Check if an agent can be spawned from the current session.",
|
|
272
|
+
description="Check if an agent can be spawned from the current session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
249
273
|
)
|
|
250
274
|
async def can_spawn_agent(parent_session_id: str) -> dict[str, Any]:
|
|
251
275
|
"""
|
|
@@ -254,12 +278,18 @@ def create_agents_registry(
|
|
|
254
278
|
This checks the agent depth limit to prevent infinite nesting.
|
|
255
279
|
|
|
256
280
|
Args:
|
|
257
|
-
parent_session_id:
|
|
281
|
+
parent_session_id: Session reference (accepts #N, N, UUID, or prefix) for the session that would spawn the agent.
|
|
258
282
|
|
|
259
283
|
Returns:
|
|
260
284
|
Dict with can_spawn boolean and reason.
|
|
261
285
|
"""
|
|
262
|
-
|
|
286
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
287
|
+
try:
|
|
288
|
+
resolved_parent_id = _resolve_session_id(parent_session_id)
|
|
289
|
+
except ValueError as e:
|
|
290
|
+
return {"can_spawn": False, "reason": str(e)}
|
|
291
|
+
|
|
292
|
+
can_spawn, reason, _parent_depth = runner.can_spawn(resolved_parent_id)
|
|
263
293
|
return {
|
|
264
294
|
"can_spawn": can_spawn,
|
|
265
295
|
"reason": reason,
|
|
@@ -267,7 +297,7 @@ def create_agents_registry(
|
|
|
267
297
|
|
|
268
298
|
@registry.tool(
|
|
269
299
|
name="list_running_agents",
|
|
270
|
-
description="List all currently running agents (in-memory process state).",
|
|
300
|
+
description="List all currently running agents (in-memory process state). Accepts #N, N, UUID, or prefix for session_id.",
|
|
271
301
|
)
|
|
272
302
|
async def list_running_agents(
|
|
273
303
|
parent_session_id: str | None = None,
|
|
@@ -280,14 +310,19 @@ def create_agents_registry(
|
|
|
280
310
|
including PIDs and process handles not stored in the database.
|
|
281
311
|
|
|
282
312
|
Args:
|
|
283
|
-
parent_session_id: Optional filter by parent
|
|
313
|
+
parent_session_id: Optional session reference (accepts #N, N, UUID, or prefix) to filter by parent.
|
|
284
314
|
mode: Optional filter by execution mode (terminal, embedded, headless).
|
|
285
315
|
|
|
286
316
|
Returns:
|
|
287
317
|
Dict with list of running agents.
|
|
288
318
|
"""
|
|
289
319
|
if parent_session_id:
|
|
290
|
-
|
|
320
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
321
|
+
try:
|
|
322
|
+
resolved_parent_id = _resolve_session_id(parent_session_id)
|
|
323
|
+
except ValueError as e:
|
|
324
|
+
return {"success": False, "error": str(e)}
|
|
325
|
+
agents = agent_registry.list_by_parent(resolved_parent_id)
|
|
291
326
|
elif mode:
|
|
292
327
|
agents = agent_registry.list_by_mode(mode)
|
|
293
328
|
else:
|
|
@@ -394,6 +429,7 @@ def create_agents_registry(
|
|
|
394
429
|
git_manager=git_manager,
|
|
395
430
|
clone_storage=clone_storage,
|
|
396
431
|
clone_manager=clone_manager,
|
|
432
|
+
session_manager=session_manager,
|
|
397
433
|
)
|
|
398
434
|
|
|
399
435
|
# Merge spawn_agent tools into agents registry
|