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.
Files changed (125) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/claude_code.py +99 -61
  3. gobby/adapters/gemini.py +140 -38
  4. gobby/agents/isolation.py +130 -0
  5. gobby/agents/registry.py +11 -0
  6. gobby/agents/session.py +1 -0
  7. gobby/agents/spawn_executor.py +43 -13
  8. gobby/agents/spawners/macos.py +26 -1
  9. gobby/app_context.py +59 -0
  10. gobby/cli/__init__.py +0 -2
  11. gobby/cli/memory.py +185 -0
  12. gobby/cli/utils.py +5 -17
  13. gobby/clones/git.py +177 -0
  14. gobby/config/features.py +0 -20
  15. gobby/config/skills.py +31 -0
  16. gobby/config/tasks.py +4 -0
  17. gobby/hooks/event_handlers/__init__.py +155 -0
  18. gobby/hooks/event_handlers/_agent.py +175 -0
  19. gobby/hooks/event_handlers/_base.py +87 -0
  20. gobby/hooks/event_handlers/_misc.py +66 -0
  21. gobby/hooks/event_handlers/_session.py +573 -0
  22. gobby/hooks/event_handlers/_tool.py +196 -0
  23. gobby/hooks/hook_manager.py +21 -1
  24. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  25. gobby/llm/claude.py +377 -42
  26. gobby/mcp_proxy/importer.py +4 -41
  27. gobby/mcp_proxy/instructions.py +2 -2
  28. gobby/mcp_proxy/manager.py +13 -3
  29. gobby/mcp_proxy/registries.py +35 -4
  30. gobby/mcp_proxy/services/recommendation.py +2 -28
  31. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  32. gobby/mcp_proxy/tools/agents.py +45 -9
  33. gobby/mcp_proxy/tools/artifacts.py +46 -12
  34. gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
  35. gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
  36. gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
  37. gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
  38. gobby/mcp_proxy/tools/spawn_agent.py +44 -6
  39. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  40. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  41. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  42. gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
  43. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  44. gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
  45. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  46. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  47. gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
  48. gobby/mcp_proxy/tools/workflows/_query.py +207 -0
  49. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  50. gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
  51. gobby/mcp_proxy/tools/worktrees.py +32 -7
  52. gobby/memory/components/__init__.py +0 -0
  53. gobby/memory/components/ingestion.py +98 -0
  54. gobby/memory/components/search.py +108 -0
  55. gobby/memory/extractor.py +15 -1
  56. gobby/memory/manager.py +16 -25
  57. gobby/paths.py +51 -0
  58. gobby/prompts/loader.py +1 -35
  59. gobby/runner.py +36 -10
  60. gobby/servers/http.py +186 -149
  61. gobby/servers/routes/admin.py +12 -0
  62. gobby/servers/routes/mcp/endpoints/execution.py +15 -7
  63. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  64. gobby/servers/routes/mcp/hooks.py +50 -3
  65. gobby/servers/websocket.py +57 -1
  66. gobby/sessions/analyzer.py +4 -4
  67. gobby/sessions/manager.py +9 -0
  68. gobby/sessions/transcripts/gemini.py +100 -34
  69. gobby/skills/parser.py +23 -0
  70. gobby/skills/sync.py +5 -4
  71. gobby/storage/artifacts.py +19 -0
  72. gobby/storage/database.py +9 -2
  73. gobby/storage/memories.py +32 -21
  74. gobby/storage/migrations.py +46 -4
  75. gobby/storage/sessions.py +4 -2
  76. gobby/storage/skills.py +87 -7
  77. gobby/tasks/external_validator.py +4 -17
  78. gobby/tasks/validation.py +13 -87
  79. gobby/tools/summarizer.py +18 -51
  80. gobby/utils/status.py +13 -0
  81. gobby/workflows/actions.py +5 -0
  82. gobby/workflows/context_actions.py +21 -24
  83. gobby/workflows/detection_helpers.py +38 -24
  84. gobby/workflows/enforcement/__init__.py +11 -1
  85. gobby/workflows/enforcement/blocking.py +109 -1
  86. gobby/workflows/enforcement/handlers.py +35 -1
  87. gobby/workflows/engine.py +96 -0
  88. gobby/workflows/evaluator.py +110 -0
  89. gobby/workflows/hooks.py +41 -0
  90. gobby/workflows/lifecycle_evaluator.py +2 -1
  91. gobby/workflows/memory_actions.py +11 -0
  92. gobby/workflows/safe_evaluator.py +8 -0
  93. gobby/workflows/summary_actions.py +123 -50
  94. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
  95. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/RECORD +99 -107
  96. gobby/cli/tui.py +0 -34
  97. gobby/hooks/event_handlers.py +0 -909
  98. gobby/mcp_proxy/tools/workflows.py +0 -973
  99. gobby/tui/__init__.py +0 -5
  100. gobby/tui/api_client.py +0 -278
  101. gobby/tui/app.py +0 -329
  102. gobby/tui/screens/__init__.py +0 -25
  103. gobby/tui/screens/agents.py +0 -333
  104. gobby/tui/screens/chat.py +0 -450
  105. gobby/tui/screens/dashboard.py +0 -377
  106. gobby/tui/screens/memory.py +0 -305
  107. gobby/tui/screens/metrics.py +0 -231
  108. gobby/tui/screens/orchestrator.py +0 -903
  109. gobby/tui/screens/sessions.py +0 -412
  110. gobby/tui/screens/tasks.py +0 -440
  111. gobby/tui/screens/workflows.py +0 -289
  112. gobby/tui/screens/worktrees.py +0 -174
  113. gobby/tui/widgets/__init__.py +0 -21
  114. gobby/tui/widgets/chat.py +0 -210
  115. gobby/tui/widgets/conductor.py +0 -104
  116. gobby/tui/widgets/menu.py +0 -132
  117. gobby/tui/widgets/message_panel.py +0 -160
  118. gobby/tui/widgets/review_gate.py +0 -224
  119. gobby/tui/widgets/task_tree.py +0 -99
  120. gobby/tui/widgets/token_budget.py +0 -166
  121. gobby/tui/ws_client.py +0 -258
  122. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
  123. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
  124. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
  125. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/top_level.txt +0 -0
@@ -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.registry import get_running_agent_registry
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 is available
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
- agent_registry=get_running_agent_registry(),
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
- try:
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
- try:
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 running children
9
+ - broadcast_to_children: Send message to all children (active in database)
10
10
 
11
- These tools resolve session relationships from RunningAgentRegistry.
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
- agent_registry: RunningAgentRegistry,
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
- agent_registry: RunningAgentRegistry for resolving parent/child relationships
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: The current (child) 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
- # Find the running agent to get parent relationship
66
- agent = agent_registry.get_by_session(session_id)
67
- if not agent:
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 {session_id} not found in running agent registry",
85
+ "error": f"Session {resolved_session_id} not found",
71
86
  }
72
87
 
73
- parent_session_id = agent.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 found for this agent",
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=session_id,
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(f"Message sent from {session_id} to parent {parent_session_id}: {msg.id}")
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(f"Failed to send message to parent: {e}")
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: The parent session ID (sender)
121
- child_session_id: The child session ID (recipient)
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
- # Verify the child exists and belongs to this parent
130
- child_agent = agent_registry.get_by_session(child_session_id)
131
- if not child_agent:
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 {child_session_id} not found in running agent registry",
161
+ "error": f"Child session {resolved_child_id} not found",
135
162
  }
136
163
 
137
- if child_agent.parent_session_id != parent_session_id:
164
+ if child_session.parent_session_id != resolved_parent_id:
138
165
  return {
139
166
  "success": False,
140
167
  "error": (
141
- f"Session {child_session_id} is not a child of {parent_session_id}. "
142
- f"Actual parent: {child_agent.parent_session_id}"
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=parent_session_id,
149
- to_session=child_session_id,
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
- f"Message sent from {parent_session_id} to child {child_session_id}: {msg.id}"
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(f"Failed to send message to child: {e}")
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: The session ID to check messages for
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=session_id,
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 running child sessions.",
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 running children.
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: The 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
- children = agent_registry.list_by_parent(parent_session_id)
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 running children found",
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=parent_session_id,
289
- to_session=child.session_id,
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.session_id}: {e}")
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
- f"Broadcast from {parent_session_id} sent to {sent_count}/{len(children)} children"
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(f"Failed to broadcast to children: {e}")
362
+ logger.error("Failed to broadcast to children: %s", e)
314
363
  return {
315
364
  "success": False,
316
365
  "error": str(e),
@@ -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: The 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
- runs = runner.list_runs(parent_session_id, status=status, limit=limit)
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: The session that would spawn the agent.
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
- can_spawn, reason, _parent_depth = runner.can_spawn(parent_session_id)
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 session.
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
- agents = agent_registry.list_by_parent(parent_session_id)
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