gobby 0.2.7__py3-none-any.whl → 0.2.8__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 (80) hide show
  1. gobby/adapters/claude_code.py +96 -35
  2. gobby/adapters/gemini.py +140 -38
  3. gobby/agents/isolation.py +130 -0
  4. gobby/agents/registry.py +11 -0
  5. gobby/agents/session.py +1 -0
  6. gobby/agents/spawn_executor.py +43 -13
  7. gobby/agents/spawners/macos.py +26 -1
  8. gobby/cli/__init__.py +0 -2
  9. gobby/cli/memory.py +185 -0
  10. gobby/clones/git.py +177 -0
  11. gobby/config/skills.py +31 -0
  12. gobby/hooks/event_handlers.py +109 -10
  13. gobby/hooks/hook_manager.py +19 -1
  14. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  15. gobby/mcp_proxy/instructions.py +2 -2
  16. gobby/mcp_proxy/registries.py +21 -4
  17. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  18. gobby/mcp_proxy/tools/agents.py +45 -9
  19. gobby/mcp_proxy/tools/artifacts.py +43 -9
  20. gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
  21. gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
  22. gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
  23. gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
  24. gobby/mcp_proxy/tools/spawn_agent.py +44 -6
  25. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  26. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  27. gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
  28. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  29. gobby/mcp_proxy/tools/workflows.py +84 -34
  30. gobby/mcp_proxy/tools/worktrees.py +32 -7
  31. gobby/memory/extractor.py +15 -1
  32. gobby/runner.py +13 -0
  33. gobby/servers/routes/mcp/hooks.py +50 -3
  34. gobby/servers/websocket.py +57 -1
  35. gobby/sessions/analyzer.py +2 -2
  36. gobby/sessions/manager.py +9 -0
  37. gobby/sessions/transcripts/gemini.py +100 -34
  38. gobby/storage/database.py +9 -2
  39. gobby/storage/memories.py +32 -21
  40. gobby/storage/migrations.py +23 -4
  41. gobby/storage/sessions.py +4 -2
  42. gobby/storage/skills.py +43 -3
  43. gobby/workflows/detection_helpers.py +38 -24
  44. gobby/workflows/enforcement/blocking.py +13 -1
  45. gobby/workflows/engine.py +93 -0
  46. gobby/workflows/evaluator.py +110 -0
  47. gobby/workflows/hooks.py +41 -0
  48. gobby/workflows/memory_actions.py +11 -0
  49. gobby/workflows/safe_evaluator.py +8 -0
  50. gobby/workflows/summary_actions.py +123 -50
  51. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/METADATA +1 -1
  52. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/RECORD +56 -80
  53. gobby/cli/tui.py +0 -34
  54. gobby/tui/__init__.py +0 -5
  55. gobby/tui/api_client.py +0 -278
  56. gobby/tui/app.py +0 -329
  57. gobby/tui/screens/__init__.py +0 -25
  58. gobby/tui/screens/agents.py +0 -333
  59. gobby/tui/screens/chat.py +0 -450
  60. gobby/tui/screens/dashboard.py +0 -377
  61. gobby/tui/screens/memory.py +0 -305
  62. gobby/tui/screens/metrics.py +0 -231
  63. gobby/tui/screens/orchestrator.py +0 -903
  64. gobby/tui/screens/sessions.py +0 -412
  65. gobby/tui/screens/tasks.py +0 -440
  66. gobby/tui/screens/workflows.py +0 -289
  67. gobby/tui/screens/worktrees.py +0 -174
  68. gobby/tui/widgets/__init__.py +0 -21
  69. gobby/tui/widgets/chat.py +0 -210
  70. gobby/tui/widgets/conductor.py +0 -104
  71. gobby/tui/widgets/menu.py +0 -132
  72. gobby/tui/widgets/message_panel.py +0 -160
  73. gobby/tui/widgets/review_gate.py +0 -224
  74. gobby/tui/widgets/task_tree.py +0 -99
  75. gobby/tui/widgets/token_budget.py +0 -166
  76. gobby/tui/ws_client.py +0 -258
  77. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/WHEEL +0 -0
  78. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  79. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  80. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -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
@@ -25,6 +25,7 @@ if TYPE_CHECKING:
25
25
  def create_artifacts_registry(
26
26
  db: LocalDatabase | None = None,
27
27
  artifact_manager: LocalArtifactManager | None = None,
28
+ session_manager: Any | None = None,
28
29
  ) -> InternalToolRegistry:
29
30
  """
30
31
  Create an artifacts tool registry with all artifact-related tools.
@@ -32,10 +33,21 @@ def create_artifacts_registry(
32
33
  Args:
33
34
  db: LocalDatabase instance (used to create artifact_manager if not provided)
34
35
  artifact_manager: LocalArtifactManager instance
36
+ session_manager: Session manager for resolving session references
35
37
 
36
38
  Returns:
37
39
  InternalToolRegistry with artifact tools registered
38
40
  """
41
+ from gobby.utils.project_context import get_project_context
42
+
43
+ def _resolve_session_id(ref: str) -> str:
44
+ """Resolve session reference (#N, N, UUID, or prefix) to UUID."""
45
+ if session_manager is None:
46
+ return ref # No resolution available, return as-is
47
+ ctx = get_project_context()
48
+ project_id = ctx.get("id") if ctx else None
49
+ return str(session_manager.resolve_session_reference(ref, project_id))
50
+
39
51
  # Create artifact manager if not provided
40
52
  if artifact_manager is None:
41
53
  if db is None:
@@ -55,7 +67,7 @@ def create_artifacts_registry(
55
67
 
56
68
  @registry.tool(
57
69
  name="search_artifacts",
58
- description="Search artifacts by content using full-text search.",
70
+ description="Search artifacts by content using full-text search. Accepts #N, N, UUID, or prefix for session_id.",
59
71
  )
60
72
  def search_artifacts(
61
73
  query: str,
@@ -68,7 +80,7 @@ def create_artifacts_registry(
68
80
 
69
81
  Args:
70
82
  query: Search query text
71
- session_id: Optional session ID to filter by
83
+ session_id: Optional session reference (accepts #N, N, UUID, or prefix) to filter by
72
84
  artifact_type: Optional artifact type to filter by (code, diff, error, etc.)
73
85
  limit: Maximum number of results (default: 50)
74
86
 
@@ -78,10 +90,18 @@ def create_artifacts_registry(
78
90
  if not query or not query.strip():
79
91
  return {"success": True, "artifacts": [], "count": 0}
80
92
 
93
+ # Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
94
+ resolved_session_id = session_id
95
+ if session_id:
96
+ try:
97
+ resolved_session_id = _resolve_session_id(session_id)
98
+ except ValueError as e:
99
+ return {"success": False, "error": str(e), "artifacts": []}
100
+
81
101
  try:
82
102
  artifacts = _artifact_manager.search_artifacts(
83
103
  query_text=query,
84
- session_id=session_id,
104
+ session_id=resolved_session_id,
85
105
  artifact_type=artifact_type,
86
106
  limit=limit,
87
107
  )
@@ -95,7 +115,7 @@ def create_artifacts_registry(
95
115
 
96
116
  @registry.tool(
97
117
  name="list_artifacts",
98
- description="List artifacts with optional filters.",
118
+ description="List artifacts with optional filters. Accepts #N, N, UUID, or prefix for session_id.",
99
119
  )
100
120
  def list_artifacts(
101
121
  session_id: str | None = None,
@@ -107,7 +127,7 @@ def create_artifacts_registry(
107
127
  List artifacts with optional filters.
108
128
 
109
129
  Args:
110
- session_id: Optional session ID to filter by
130
+ session_id: Optional session reference (accepts #N, N, UUID, or prefix) to filter by
111
131
  artifact_type: Optional artifact type to filter by
112
132
  limit: Maximum number of results (default: 100)
113
133
  offset: Offset for pagination (default: 0)
@@ -115,9 +135,17 @@ def create_artifacts_registry(
115
135
  Returns:
116
136
  Dict with success status and list of artifacts
117
137
  """
138
+ # Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
139
+ resolved_session_id = session_id
140
+ if session_id:
141
+ try:
142
+ resolved_session_id = _resolve_session_id(session_id)
143
+ except ValueError as e:
144
+ return {"success": False, "error": str(e), "artifacts": []}
145
+
118
146
  try:
119
147
  artifacts = _artifact_manager.list_artifacts(
120
- session_id=session_id,
148
+ session_id=resolved_session_id,
121
149
  artifact_type=artifact_type,
122
150
  limit=limit,
123
151
  offset=offset,
@@ -161,7 +189,7 @@ def create_artifacts_registry(
161
189
 
162
190
  @registry.tool(
163
191
  name="get_timeline",
164
- description="Get artifacts for a session in chronological order.",
192
+ description="Get artifacts for a session in chronological order. Accepts #N, N, UUID, or prefix for session_id.",
165
193
  )
166
194
  def get_timeline(
167
195
  session_id: str | None = None,
@@ -172,7 +200,7 @@ def create_artifacts_registry(
172
200
  Get artifacts for a session in chronological order (oldest first).
173
201
 
174
202
  Args:
175
- session_id: Required session ID to get timeline for
203
+ session_id: Required session reference (accepts #N, N, UUID, or prefix) to get timeline for
176
204
  artifact_type: Optional artifact type to filter by
177
205
  limit: Maximum number of results (default: 100)
178
206
 
@@ -186,10 +214,16 @@ def create_artifacts_registry(
186
214
  "artifacts": [],
187
215
  }
188
216
 
217
+ # Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
218
+ try:
219
+ resolved_session_id = _resolve_session_id(session_id)
220
+ except ValueError as e:
221
+ return {"success": False, "error": str(e), "artifacts": []}
222
+
189
223
  try:
190
224
  # Get artifacts (list_artifacts returns newest first by default)
191
225
  artifacts = _artifact_manager.list_artifacts(
192
- session_id=session_id,
226
+ session_id=resolved_session_id,
193
227
  artifact_type=artifact_type,
194
228
  limit=limit,
195
229
  offset=0,