gobby 0.2.6__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 (198) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +96 -35
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/adapters/gemini.py +140 -38
  10. gobby/agents/definitions.py +11 -1
  11. gobby/agents/isolation.py +525 -0
  12. gobby/agents/registry.py +11 -0
  13. gobby/agents/sandbox.py +261 -0
  14. gobby/agents/session.py +1 -0
  15. gobby/agents/spawn.py +42 -287
  16. gobby/agents/spawn_executor.py +415 -0
  17. gobby/agents/spawners/__init__.py +24 -0
  18. gobby/agents/spawners/command_builder.py +189 -0
  19. gobby/agents/spawners/embedded.py +21 -2
  20. gobby/agents/spawners/headless.py +21 -2
  21. gobby/agents/spawners/macos.py +26 -1
  22. gobby/agents/spawners/prompt_manager.py +125 -0
  23. gobby/cli/__init__.py +0 -2
  24. gobby/cli/install.py +4 -4
  25. gobby/cli/installers/claude.py +6 -0
  26. gobby/cli/installers/gemini.py +6 -0
  27. gobby/cli/installers/shared.py +103 -4
  28. gobby/cli/memory.py +185 -0
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/utils.py +9 -2
  31. gobby/clones/git.py +177 -0
  32. gobby/config/__init__.py +12 -97
  33. gobby/config/app.py +10 -94
  34. gobby/config/extensions.py +2 -2
  35. gobby/config/features.py +7 -130
  36. gobby/config/skills.py +31 -0
  37. gobby/config/tasks.py +4 -28
  38. gobby/hooks/__init__.py +0 -13
  39. gobby/hooks/event_handlers.py +150 -8
  40. gobby/hooks/hook_manager.py +21 -3
  41. gobby/hooks/plugins.py +1 -1
  42. gobby/hooks/webhooks.py +1 -1
  43. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  44. gobby/llm/resolver.py +3 -2
  45. gobby/mcp_proxy/importer.py +62 -4
  46. gobby/mcp_proxy/instructions.py +4 -2
  47. gobby/mcp_proxy/registries.py +22 -8
  48. gobby/mcp_proxy/services/recommendation.py +43 -11
  49. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  50. gobby/mcp_proxy/tools/agents.py +76 -740
  51. gobby/mcp_proxy/tools/artifacts.py +43 -9
  52. gobby/mcp_proxy/tools/clones.py +0 -385
  53. gobby/mcp_proxy/tools/memory.py +2 -2
  54. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  55. gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
  56. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  57. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  58. gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
  59. gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
  60. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  61. gobby/mcp_proxy/tools/spawn_agent.py +455 -0
  62. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  63. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  64. gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
  65. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  66. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  67. gobby/mcp_proxy/tools/workflows.py +84 -34
  68. gobby/mcp_proxy/tools/worktrees.py +32 -350
  69. gobby/memory/extractor.py +15 -1
  70. gobby/memory/ingestion/__init__.py +5 -0
  71. gobby/memory/ingestion/multimodal.py +221 -0
  72. gobby/memory/manager.py +62 -283
  73. gobby/memory/search/__init__.py +10 -0
  74. gobby/memory/search/coordinator.py +248 -0
  75. gobby/memory/services/__init__.py +5 -0
  76. gobby/memory/services/crossref.py +142 -0
  77. gobby/prompts/loader.py +5 -2
  78. gobby/runner.py +13 -0
  79. gobby/servers/http.py +1 -4
  80. gobby/servers/routes/admin.py +14 -0
  81. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  82. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  83. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  84. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  85. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  86. gobby/servers/routes/mcp/hooks.py +51 -4
  87. gobby/servers/routes/mcp/tools.py +48 -1506
  88. gobby/servers/websocket.py +57 -1
  89. gobby/sessions/analyzer.py +2 -2
  90. gobby/sessions/lifecycle.py +1 -1
  91. gobby/sessions/manager.py +9 -0
  92. gobby/sessions/processor.py +10 -0
  93. gobby/sessions/transcripts/base.py +1 -0
  94. gobby/sessions/transcripts/claude.py +15 -5
  95. gobby/sessions/transcripts/gemini.py +100 -34
  96. gobby/skills/parser.py +30 -2
  97. gobby/storage/database.py +9 -2
  98. gobby/storage/memories.py +32 -21
  99. gobby/storage/migrations.py +174 -368
  100. gobby/storage/sessions.py +45 -7
  101. gobby/storage/skills.py +80 -7
  102. gobby/storage/tasks/_lifecycle.py +18 -3
  103. gobby/sync/memories.py +1 -1
  104. gobby/tasks/external_validator.py +1 -1
  105. gobby/tasks/validation.py +22 -20
  106. gobby/tools/summarizer.py +91 -10
  107. gobby/utils/project_context.py +2 -3
  108. gobby/utils/status.py +13 -0
  109. gobby/workflows/actions.py +221 -1217
  110. gobby/workflows/artifact_actions.py +31 -0
  111. gobby/workflows/autonomous_actions.py +11 -0
  112. gobby/workflows/context_actions.py +50 -1
  113. gobby/workflows/detection_helpers.py +38 -24
  114. gobby/workflows/enforcement/__init__.py +47 -0
  115. gobby/workflows/enforcement/blocking.py +281 -0
  116. gobby/workflows/enforcement/commit_policy.py +283 -0
  117. gobby/workflows/enforcement/handlers.py +269 -0
  118. gobby/workflows/enforcement/task_policy.py +542 -0
  119. gobby/workflows/engine.py +93 -0
  120. gobby/workflows/evaluator.py +110 -0
  121. gobby/workflows/git_utils.py +106 -0
  122. gobby/workflows/hooks.py +41 -0
  123. gobby/workflows/llm_actions.py +30 -0
  124. gobby/workflows/mcp_actions.py +20 -1
  125. gobby/workflows/memory_actions.py +91 -0
  126. gobby/workflows/safe_evaluator.py +191 -0
  127. gobby/workflows/session_actions.py +44 -0
  128. gobby/workflows/state_actions.py +60 -1
  129. gobby/workflows/stop_signal_actions.py +55 -0
  130. gobby/workflows/summary_actions.py +217 -51
  131. gobby/workflows/task_sync_actions.py +347 -0
  132. gobby/workflows/todo_actions.py +34 -1
  133. gobby/workflows/webhook_actions.py +185 -0
  134. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
  135. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
  136. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
  137. gobby/adapters/codex.py +0 -1332
  138. gobby/cli/tui.py +0 -34
  139. gobby/install/claude/commands/gobby/bug.md +0 -51
  140. gobby/install/claude/commands/gobby/chore.md +0 -51
  141. gobby/install/claude/commands/gobby/epic.md +0 -52
  142. gobby/install/claude/commands/gobby/eval.md +0 -235
  143. gobby/install/claude/commands/gobby/feat.md +0 -49
  144. gobby/install/claude/commands/gobby/nit.md +0 -52
  145. gobby/install/claude/commands/gobby/ref.md +0 -52
  146. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  147. gobby/prompts/defaults/expansion/system.md +0 -119
  148. gobby/prompts/defaults/expansion/user.md +0 -48
  149. gobby/prompts/defaults/external_validation/agent.md +0 -72
  150. gobby/prompts/defaults/external_validation/external.md +0 -63
  151. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  152. gobby/prompts/defaults/external_validation/system.md +0 -6
  153. gobby/prompts/defaults/features/import_mcp.md +0 -22
  154. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  155. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  156. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  157. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  158. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  159. gobby/prompts/defaults/features/server_description.md +0 -20
  160. gobby/prompts/defaults/features/server_description_system.md +0 -6
  161. gobby/prompts/defaults/features/task_description.md +0 -31
  162. gobby/prompts/defaults/features/task_description_system.md +0 -6
  163. gobby/prompts/defaults/features/tool_summary.md +0 -17
  164. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  165. gobby/prompts/defaults/handoff/compact.md +0 -63
  166. gobby/prompts/defaults/handoff/session_end.md +0 -57
  167. gobby/prompts/defaults/memory/extract.md +0 -61
  168. gobby/prompts/defaults/research/step.md +0 -58
  169. gobby/prompts/defaults/validation/criteria.md +0 -47
  170. gobby/prompts/defaults/validation/validate.md +0 -38
  171. gobby/storage/migrations_legacy.py +0 -1359
  172. gobby/tui/__init__.py +0 -5
  173. gobby/tui/api_client.py +0 -278
  174. gobby/tui/app.py +0 -329
  175. gobby/tui/screens/__init__.py +0 -25
  176. gobby/tui/screens/agents.py +0 -333
  177. gobby/tui/screens/chat.py +0 -450
  178. gobby/tui/screens/dashboard.py +0 -377
  179. gobby/tui/screens/memory.py +0 -305
  180. gobby/tui/screens/metrics.py +0 -231
  181. gobby/tui/screens/orchestrator.py +0 -903
  182. gobby/tui/screens/sessions.py +0 -412
  183. gobby/tui/screens/tasks.py +0 -440
  184. gobby/tui/screens/workflows.py +0 -289
  185. gobby/tui/screens/worktrees.py +0 -174
  186. gobby/tui/widgets/__init__.py +0 -21
  187. gobby/tui/widgets/chat.py +0 -210
  188. gobby/tui/widgets/conductor.py +0 -104
  189. gobby/tui/widgets/menu.py +0 -132
  190. gobby/tui/widgets/message_panel.py +0 -160
  191. gobby/tui/widgets/review_gate.py +0 -224
  192. gobby/tui/widgets/task_tree.py +0 -99
  193. gobby/tui/widgets/token_budget.py +0 -166
  194. gobby/tui/ws_client.py +0 -258
  195. gobby/workflows/task_enforcement_actions.py +0 -1343
  196. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  197. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  198. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,7 @@
2
2
  Internal MCP tools for Gobby Agent System.
3
3
 
4
4
  Exposes functionality for:
5
- - Starting agents (spawn subagents with prompts)
5
+ - Spawning agents (via spawn_agent unified tool)
6
6
  - Getting agent results (retrieve completed run output)
7
7
  - Listing agents (view runs for a session)
8
8
  - Cancelling agents (stop running agents)
@@ -14,69 +14,62 @@ via the downstream proxy pattern (call_tool, list_tools, get_tool_schema).
14
14
  from __future__ import annotations
15
15
 
16
16
  import logging
17
- import socket
18
- from collections.abc import Callable
19
- from pathlib import Path
20
17
  from typing import TYPE_CHECKING, Any
21
18
 
22
- from gobby.agents.context import (
23
- ContextResolutionError,
24
- ContextResolver,
25
- format_injected_prompt,
26
- )
27
19
  from gobby.agents.registry import (
28
- RunningAgent,
29
20
  RunningAgentRegistry,
30
21
  get_running_agent_registry,
31
22
  )
32
- from gobby.agents.spawn import (
33
- EmbeddedSpawner,
34
- HeadlessSpawner,
35
- TerminalSpawner,
36
- )
37
23
  from gobby.mcp_proxy.tools.internal import InternalToolRegistry
38
- from gobby.utils.project_context import get_project_context
39
24
 
40
25
  if TYPE_CHECKING:
41
26
  from gobby.agents.runner import AgentRunner
42
- from gobby.config.app import ContextInjectionConfig
43
- from gobby.llm.executor import ToolResult
44
- from gobby.mcp_proxy.services.tool_proxy import ToolProxyService
45
- from gobby.storage.session_messages import LocalSessionMessageManager
46
- from gobby.storage.sessions import LocalSessionManager
47
27
 
48
28
  logger = logging.getLogger(__name__)
49
29
 
50
30
 
51
31
  def create_agents_registry(
52
32
  runner: AgentRunner,
53
- session_manager: LocalSessionManager | None = None,
54
- message_manager: LocalSessionMessageManager | None = None,
55
- context_config: ContextInjectionConfig | None = None,
56
- get_session_context: Any | None = None,
57
33
  running_registry: RunningAgentRegistry | None = None,
58
- tool_proxy_getter: Callable[[], ToolProxyService | None] | None = None,
59
34
  workflow_state_manager: Any | None = None,
35
+ session_manager: Any | None = None,
36
+ # spawn_agent dependencies
37
+ agent_loader: Any | None = None,
38
+ task_manager: Any | None = None,
39
+ worktree_storage: Any | None = None,
40
+ git_manager: Any | None = None,
41
+ clone_storage: Any | None = None,
42
+ clone_manager: Any | None = None,
60
43
  ) -> InternalToolRegistry:
61
44
  """
62
45
  Create an agent tool registry with all agent-related tools.
63
46
 
64
47
  Args:
65
48
  runner: AgentRunner instance for executing agents.
66
- session_manager: Session manager for context resolution.
67
- message_manager: Message manager for transcript resolution.
68
- context_config: Context injection configuration.
69
- get_session_context: Optional callable returning current session context.
70
49
  running_registry: Optional in-memory registry for running agents.
71
- tool_proxy_getter: Optional callable that returns ToolProxyService for
72
- routing tool calls in in-process agents. If not provided, tool calls
73
- will fail with "tool not available".
74
50
  workflow_state_manager: Optional WorkflowStateManager for stopping workflows
75
51
  when agents are killed. If not provided, workflow stop will be skipped.
52
+ session_manager: Optional LocalSessionManager for resolving session references.
53
+ agent_loader: Agent definition loader for spawn_agent.
54
+ task_manager: Task manager for spawn_agent task resolution.
55
+ worktree_storage: Worktree storage for spawn_agent isolation.
56
+ git_manager: Git manager for spawn_agent isolation.
57
+ clone_storage: Clone storage for spawn_agent isolation.
58
+ clone_manager: Clone git manager for spawn_agent isolation.
76
59
 
77
60
  Returns:
78
61
  InternalToolRegistry with all agent tools registered.
79
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
+
80
73
  registry = InternalToolRegistry(
81
74
  name="gobby-agents",
82
75
  description="Agent spawning - start, monitor, and manage subagents",
@@ -85,704 +78,6 @@ def create_agents_registry(
85
78
  # Use provided registry or global singleton
86
79
  agent_registry = running_registry or get_running_agent_registry()
87
80
 
88
- # Create context resolver if managers are provided
89
- context_resolver: ContextResolver | None = None
90
- context_enabled = True # Default enabled
91
- context_template: str | None = None # Custom template for injection
92
- if session_manager and message_manager:
93
- # Use config values if provided, otherwise use defaults
94
- if context_config:
95
- context_enabled = context_config.enabled
96
- context_template = context_config.context_template
97
- context_resolver = ContextResolver(
98
- session_manager=session_manager,
99
- message_manager=message_manager,
100
- project_path=None, # Will be set per-request
101
- max_file_size=context_config.max_file_size,
102
- max_content_size=context_config.max_content_size,
103
- max_transcript_messages=context_config.max_transcript_messages,
104
- truncation_suffix=context_config.truncation_suffix,
105
- )
106
- else:
107
- context_resolver = ContextResolver(
108
- session_manager=session_manager,
109
- message_manager=message_manager,
110
- project_path=None, # Will be set per-request
111
- )
112
-
113
- @registry.tool(
114
- name="start_agent",
115
- description=(
116
- "Spawn a subagent to execute a task. Can use a named agent definition "
117
- "(e.g. 'validation-runner') or raw parameters. "
118
- "Infers context from current project/session. "
119
- "Use get_agent_result to poll for completion."
120
- ),
121
- )
122
- async def start_agent(
123
- prompt: str,
124
- workflow: str | None = None,
125
- task: str | None = None,
126
- agent: str | None = None,
127
- session_context: str = "summary_markdown",
128
- mode: str = "terminal",
129
- terminal: str = "auto",
130
- provider: str | None = None,
131
- model: str | None = None,
132
- worktree_id: str | None = None,
133
- timeout: float = 120.0,
134
- max_turns: int = 10,
135
- # Optional explicit context (usually inferred)
136
- parent_session_id: str | None = None,
137
- project_id: str | None = None,
138
- machine_id: str | None = None,
139
- source: str = "claude",
140
- ) -> dict[str, Any]:
141
- """
142
- Start a new agent to execute a task.
143
-
144
- Args:
145
- prompt: The task/prompt for the agent.
146
- workflow: Workflow name or path to execute.
147
- task: Task ID or 'next' for auto-select.
148
- agent: Named agent definition to use.
149
- session_context: Context source (summary_markdown, compact_markdown,
150
- session_id:<id>, transcript:<n>, file:<path>).
151
- mode: Execution mode (in_process, terminal, embedded, headless).
152
- terminal: Terminal for terminal/embedded modes (auto, ghostty, iterm, etc.).
153
- provider: LLM provider (claude, gemini, etc.). Defaults to claude.
154
- model: Optional model override.
155
- worktree_id: Existing worktree to use for terminal mode.
156
- timeout: Execution timeout in seconds (default: 120).
157
- max_turns: Maximum turns (default: 10).
158
- parent_session_id: Explicit parent session ID (usually inferred).
159
- project_id: Explicit project ID (usually inferred from context).
160
- machine_id: Explicit machine ID (usually inferred from hostname).
161
- source: CLI source (claude, gemini, codex).
162
-
163
- Returns:
164
- Dict with run_id, child_session_id, status.
165
- """
166
- from gobby.agents.runner import AgentConfig
167
-
168
- # Validate mode
169
- supported_modes = {"in_process", "terminal", "embedded", "headless"}
170
- if mode not in supported_modes:
171
- return {
172
- "success": False,
173
- "error": f"Invalid mode '{mode}'. Supported: {supported_modes}",
174
- }
175
-
176
- # Validate workflow (reject lifecycle workflows)
177
- if workflow:
178
- from gobby.workflows.loader import WorkflowLoader
179
-
180
- workflow_loader = WorkflowLoader()
181
- is_valid, error_msg = workflow_loader.validate_workflow_for_agent(workflow)
182
- if not is_valid:
183
- return {
184
- "success": False,
185
- "error": error_msg,
186
- }
187
-
188
- # Infer context from project if not provided
189
- ctx = get_project_context()
190
- if project_id is None:
191
- if ctx:
192
- project_id = ctx.get("id")
193
- project_path = ctx.get("project_path")
194
- else:
195
- return {
196
- "success": False,
197
- "error": "No project context found. Run from a Gobby project directory.",
198
- }
199
- else:
200
- # project_id was provided - try to get project_path from context if it matches
201
- if ctx and ctx.get("id") == project_id:
202
- project_path = ctx.get("project_path")
203
- else:
204
- project_path = None
205
-
206
- # Infer machine_id from hostname if not provided
207
- if machine_id is None:
208
- machine_id = socket.gethostname()
209
-
210
- # Parent session is required for depth checking
211
- if parent_session_id is None:
212
- # TODO: In future, could look up current active session for project
213
- return {
214
- "success": False,
215
- "error": "parent_session_id is required (session context inference not yet implemented)",
216
- }
217
-
218
- # Check if spawning is allowed
219
- can_spawn, reason, _parent_depth = runner.can_spawn(parent_session_id)
220
- if not can_spawn:
221
- return {
222
- "success": False,
223
- "error": reason,
224
- }
225
-
226
- # Resolve context and inject into prompt
227
- effective_prompt = prompt
228
- context_was_injected = False
229
- if context_resolver and context_enabled and session_context:
230
- try:
231
- # Update resolver's project path for file resolution
232
- context_resolver._project_path = Path(project_path) if project_path else None
233
-
234
- resolved_context = await context_resolver.resolve(
235
- session_context, parent_session_id
236
- )
237
- if resolved_context:
238
- effective_prompt = format_injected_prompt(
239
- resolved_context, prompt, template=context_template
240
- )
241
- context_was_injected = True
242
- logger.info(
243
- f"Injected context from '{session_context}' into agent prompt "
244
- f"({len(resolved_context)} chars)"
245
- )
246
- except ContextResolutionError as e:
247
- logger.warning(f"Context resolution failed: {e}")
248
- # Continue with original prompt - context injection is best-effort
249
- pass
250
-
251
- # Use provided provider or default
252
- effective_provider = provider or "claude"
253
-
254
- config = AgentConfig(
255
- prompt=effective_prompt,
256
- parent_session_id=parent_session_id,
257
- project_id=project_id,
258
- machine_id=machine_id,
259
- source=source,
260
- workflow=workflow,
261
- task=task,
262
- agent=agent,
263
- session_context=session_context,
264
- mode=mode,
265
- terminal=terminal,
266
- worktree_id=worktree_id,
267
- provider=effective_provider,
268
- model=model,
269
- max_turns=max_turns,
270
- timeout=timeout,
271
- project_path=project_path,
272
- context_injected=context_was_injected,
273
- )
274
-
275
- # Handle different execution modes
276
- if mode == "in_process":
277
- # In-process mode: run directly via runner
278
- async def tool_handler(tool_name: str, arguments: dict[str, Any]) -> ToolResult:
279
- from gobby.llm.executor import ToolResult
280
-
281
- # Get tool proxy for routing calls
282
- tool_proxy = tool_proxy_getter() if tool_proxy_getter else None
283
- if tool_proxy is None:
284
- return ToolResult(
285
- tool_name=tool_name,
286
- success=False,
287
- error=f"Tool proxy not configured - cannot route tool {tool_name}",
288
- )
289
-
290
- # Route the tool call through the MCP proxy
291
- try:
292
- result = await tool_proxy.call_tool_by_name(tool_name, arguments)
293
-
294
- # Handle error response format from call_tool_by_name
295
- if isinstance(result, dict) and result.get("success") is False:
296
- return ToolResult(
297
- tool_name=tool_name,
298
- success=False,
299
- error=result.get("error", f"Tool {tool_name} failed"),
300
- )
301
-
302
- # Successful tool call
303
- return ToolResult(
304
- tool_name=tool_name,
305
- success=True,
306
- result=result,
307
- )
308
- except Exception as e:
309
- logger.warning(f"Tool call failed for {tool_name}: {e}")
310
- return ToolResult(
311
- tool_name=tool_name,
312
- success=False,
313
- error=str(e),
314
- )
315
-
316
- # Load available tools for the agent
317
- from gobby.llm.executor import ToolSchema
318
-
319
- tool_schemas: list[ToolSchema] = []
320
- tool_proxy = tool_proxy_getter() if tool_proxy_getter else None
321
- if tool_proxy:
322
- # Get internal servers that have tools
323
- internal_servers = ["gobby-tasks", "gobby-memory", "gobby-sessions"]
324
- for srv in internal_servers:
325
- try:
326
- tools_result = await tool_proxy.list_tools(srv)
327
- if tools_result.get("success"):
328
- for tool_brief in tools_result.get("tools", []):
329
- # Get full schema for each tool
330
- schema_result = await tool_proxy.get_tool_schema(
331
- srv, tool_brief["name"]
332
- )
333
- if schema_result.get("success"):
334
- tool_data = schema_result.get("tool", {})
335
- tool_schemas.append(
336
- ToolSchema(
337
- name=tool_brief["name"],
338
- description=tool_brief.get("brief", ""),
339
- input_schema=tool_data.get("inputSchema", {}),
340
- server_name=srv,
341
- )
342
- )
343
- except Exception as e:
344
- logger.debug(f"Could not load tools from {srv}: {e}")
345
-
346
- # Set tools on config
347
- config.tools = tool_schemas
348
- logger.info(f"Loaded {len(tool_schemas)} tools for in-process agent")
349
-
350
- result = await runner.run(config, tool_handler=tool_handler)
351
-
352
- return {
353
- "success": result.status in ("success", "partial"),
354
- "run_id": result.run_id,
355
- "status": result.status,
356
- "output": result.output,
357
- "error": result.error,
358
- "turns_used": result.turns_used,
359
- "tool_calls_count": len(result.tool_calls),
360
- }
361
-
362
- # Special handling for Gemini terminal mode: requires preflight session capture
363
- # Gemini CLI in interactive mode can't introspect its session_id, so we:
364
- # 1. Launch preflight to capture session_id from stream-json output
365
- # 2. Create Gobby session with external_id = gemini's session_id
366
- # 3. Launch interactive with -r {session_id} to resume
367
- if mode == "terminal" and effective_provider == "gemini":
368
- from gobby.agents.spawn import (
369
- build_gemini_command_with_resume,
370
- prepare_gemini_spawn_with_preflight,
371
- )
372
-
373
- # Ensure project_id is non-None for spawning
374
- if project_id is None:
375
- return {
376
- "success": False,
377
- "error": "project_id is required for spawning Gemini agent",
378
- }
379
-
380
- # Determine working directory
381
- cwd = project_path or "."
382
-
383
- try:
384
- # Preflight capture: gets Gemini's session_id and creates linked Gobby session
385
- spawn_context = await prepare_gemini_spawn_with_preflight(
386
- session_manager=runner._child_session_manager,
387
- parent_session_id=parent_session_id,
388
- project_id=project_id,
389
- machine_id=socket.gethostname(),
390
- workflow_name=workflow,
391
- git_branch=None, # Will be detected by hook
392
- )
393
- except FileNotFoundError as e:
394
- return {
395
- "success": False,
396
- "error": str(e),
397
- }
398
- except Exception as e:
399
- logger.error(f"Gemini preflight capture failed: {e}", exc_info=True)
400
- return {
401
- "success": False,
402
- "error": f"Gemini preflight capture failed: {e}",
403
- }
404
-
405
- # Extract IDs from prepared spawn context
406
- gobby_session_id = spawn_context.session_id
407
- gemini_session_id = spawn_context.env_vars["GOBBY_GEMINI_EXTERNAL_ID"]
408
-
409
- # Build command with session context injected into prompt
410
- # build_gemini_command_with_resume handles the context prefix
411
- cmd = build_gemini_command_with_resume(
412
- gemini_external_id=gemini_session_id,
413
- prompt=effective_prompt,
414
- auto_approve=True, # Subagents need to work autonomously
415
- gobby_session_id=gobby_session_id,
416
- )
417
-
418
- # Spawn in terminal
419
- terminal_spawner = TerminalSpawner()
420
- terminal_result = terminal_spawner.spawn(
421
- command=cmd,
422
- cwd=cwd,
423
- terminal=terminal,
424
- )
425
-
426
- if not terminal_result.success:
427
- return {
428
- "success": False,
429
- "error": terminal_result.error or terminal_result.message,
430
- "child_session_id": gobby_session_id,
431
- }
432
-
433
- # Register in running agents registry
434
- registry = get_running_agent_registry()
435
- running_agent = RunningAgent(
436
- run_id=f"gemini-{gemini_session_id[:8]}",
437
- session_id=gobby_session_id,
438
- parent_session_id=parent_session_id,
439
- pid=terminal_result.pid,
440
- mode="terminal",
441
- provider="gemini",
442
- workflow_name=workflow,
443
- )
444
- registry.add(running_agent)
445
-
446
- return {
447
- "success": True,
448
- "run_id": running_agent.run_id,
449
- "child_session_id": gobby_session_id,
450
- "gemini_session_id": gemini_session_id,
451
- "mode": "terminal",
452
- "message": (f"Gemini agent spawned in terminal with session {gobby_session_id}"),
453
- "pid": terminal_result.pid,
454
- }
455
-
456
- # Special handling for Codex terminal mode: requires preflight session capture
457
- # Codex outputs session_id in startup banner, which we parse from `codex exec "exit"`
458
- if mode == "terminal" and effective_provider == "codex":
459
- from gobby.agents.spawn import (
460
- build_codex_command_with_resume,
461
- prepare_codex_spawn_with_preflight,
462
- )
463
-
464
- # Ensure project_id is non-None for spawning
465
- if project_id is None:
466
- return {
467
- "success": False,
468
- "error": "project_id is required for spawning Codex agent",
469
- }
470
-
471
- # Determine working directory
472
- cwd = project_path or "."
473
-
474
- try:
475
- # Preflight capture: gets Codex's session_id and creates linked Gobby session
476
- spawn_context = await prepare_codex_spawn_with_preflight(
477
- session_manager=runner._child_session_manager,
478
- parent_session_id=parent_session_id,
479
- project_id=project_id,
480
- machine_id=socket.gethostname(),
481
- workflow_name=workflow,
482
- git_branch=None, # Will be detected by hook
483
- )
484
- except FileNotFoundError as e:
485
- return {
486
- "success": False,
487
- "error": str(e),
488
- }
489
- except Exception as e:
490
- logger.error(f"Codex preflight capture failed: {e}", exc_info=True)
491
- return {
492
- "success": False,
493
- "error": f"Codex preflight capture failed: {e}",
494
- }
495
-
496
- # Extract IDs from prepared spawn context
497
- gobby_session_id = spawn_context.session_id
498
- codex_session_id = spawn_context.env_vars["GOBBY_CODEX_EXTERNAL_ID"]
499
-
500
- # Build command with session context injected into prompt
501
- # build_codex_command_with_resume handles the context prefix
502
- cmd = build_codex_command_with_resume(
503
- codex_external_id=codex_session_id,
504
- prompt=effective_prompt,
505
- auto_approve=True, # --full-auto for sandboxed autonomy
506
- gobby_session_id=gobby_session_id,
507
- working_directory=cwd,
508
- )
509
-
510
- # Spawn in terminal
511
- terminal_spawner = TerminalSpawner()
512
- terminal_result = terminal_spawner.spawn(
513
- command=cmd,
514
- cwd=cwd,
515
- terminal=terminal,
516
- )
517
-
518
- if not terminal_result.success:
519
- return {
520
- "success": False,
521
- "error": terminal_result.error or terminal_result.message,
522
- "child_session_id": gobby_session_id,
523
- }
524
-
525
- # Register in running agents registry
526
- registry = get_running_agent_registry()
527
- running_agent = RunningAgent(
528
- run_id=f"codex-{codex_session_id[:8]}",
529
- session_id=gobby_session_id,
530
- parent_session_id=parent_session_id,
531
- pid=terminal_result.pid,
532
- mode="terminal",
533
- provider="codex",
534
- workflow_name=workflow,
535
- )
536
- registry.add(running_agent)
537
-
538
- return {
539
- "success": True,
540
- "run_id": running_agent.run_id,
541
- "child_session_id": gobby_session_id,
542
- "codex_session_id": codex_session_id,
543
- "mode": "terminal",
544
- "message": (f"Codex agent spawned in terminal with session {gobby_session_id}"),
545
- "pid": terminal_result.pid,
546
- }
547
-
548
- # Terminal, embedded, or headless mode: prepare run then spawn
549
- # Use prepare_run to create session and run records
550
- from gobby.llm.executor import AgentResult
551
-
552
- prepare_result = runner.prepare_run(config)
553
- if isinstance(prepare_result, AgentResult):
554
- # prepare_run returns AgentResult on error
555
- return {
556
- "success": False,
557
- "error": prepare_result.error,
558
- }
559
-
560
- # Successfully prepared - we have context with session and run
561
- context = prepare_result
562
-
563
- # Validate context has required session and run (should always be set after prepare_run)
564
- if context.session is None or context.run is None:
565
- return {
566
- "success": False,
567
- "error": "Internal error: context missing session or run after prepare_run",
568
- }
569
-
570
- # Type narrowing: assign to non-optional variables
571
- child_session = context.session
572
- agent_run = context.run
573
-
574
- # Determine working directory
575
- cwd = project_path or "."
576
-
577
- # Ensure project_id is non-None for spawn calls
578
- if project_id is None:
579
- return {
580
- "success": False,
581
- "error": "project_id is required for spawning",
582
- }
583
-
584
- if mode == "terminal":
585
- # Spawn in external terminal
586
- terminal_spawner = TerminalSpawner()
587
- terminal_result = terminal_spawner.spawn_agent(
588
- cli=effective_provider, # claude, gemini, codex
589
- cwd=cwd,
590
- session_id=child_session.id,
591
- parent_session_id=parent_session_id,
592
- agent_run_id=agent_run.id,
593
- project_id=project_id,
594
- workflow_name=workflow,
595
- agent_depth=child_session.agent_depth,
596
- max_agent_depth=runner._child_session_manager.max_agent_depth,
597
- terminal=terminal,
598
- prompt=effective_prompt,
599
- )
600
-
601
- if not terminal_result.success:
602
- return {
603
- "success": False,
604
- "error": terminal_result.error or terminal_result.message,
605
- "run_id": agent_run.id,
606
- "child_session_id": child_session.id,
607
- }
608
-
609
- # Register in running agents registry
610
- running_agent = RunningAgent(
611
- run_id=agent_run.id,
612
- session_id=child_session.id,
613
- parent_session_id=parent_session_id,
614
- mode="terminal",
615
- pid=terminal_result.pid,
616
- terminal_type=terminal_result.terminal_type,
617
- provider=effective_provider,
618
- workflow_name=workflow,
619
- worktree_id=worktree_id,
620
- )
621
- agent_registry.add(running_agent)
622
-
623
- return {
624
- "success": True,
625
- "run_id": agent_run.id,
626
- "child_session_id": child_session.id,
627
- "status": "pending",
628
- "message": f"Agent spawned in {terminal_result.terminal_type} (PID: {terminal_result.pid})",
629
- "terminal_type": terminal_result.terminal_type,
630
- "pid": terminal_result.pid,
631
- }
632
-
633
- elif mode == "embedded":
634
- # Spawn with PTY for UI attachment
635
- embedded_spawner = EmbeddedSpawner()
636
- embedded_result = embedded_spawner.spawn_agent(
637
- cli=effective_provider,
638
- cwd=cwd,
639
- session_id=child_session.id,
640
- parent_session_id=parent_session_id,
641
- agent_run_id=agent_run.id,
642
- project_id=project_id,
643
- workflow_name=workflow,
644
- agent_depth=child_session.agent_depth,
645
- max_agent_depth=runner._child_session_manager.max_agent_depth,
646
- prompt=effective_prompt,
647
- )
648
-
649
- if not embedded_result.success:
650
- return {
651
- "success": False,
652
- "error": embedded_result.error or embedded_result.message,
653
- "run_id": agent_run.id,
654
- "child_session_id": child_session.id,
655
- }
656
-
657
- # Register in running agents registry
658
- running_agent = RunningAgent(
659
- run_id=agent_run.id,
660
- session_id=child_session.id,
661
- parent_session_id=parent_session_id,
662
- mode="embedded",
663
- pid=embedded_result.pid,
664
- master_fd=embedded_result.master_fd,
665
- provider=effective_provider,
666
- workflow_name=workflow,
667
- worktree_id=worktree_id,
668
- )
669
- agent_registry.add(running_agent)
670
-
671
- return {
672
- "success": True,
673
- "run_id": agent_run.id,
674
- "child_session_id": child_session.id,
675
- "status": "pending",
676
- "message": f"Agent spawned with PTY (PID: {embedded_result.pid})",
677
- "pid": embedded_result.pid,
678
- "master_fd": embedded_result.master_fd,
679
- }
680
-
681
- else: # headless mode
682
- # Spawn headless with output capture
683
- headless_spawner = HeadlessSpawner()
684
- headless_result = headless_spawner.spawn_agent(
685
- cli=effective_provider,
686
- cwd=cwd,
687
- session_id=child_session.id,
688
- parent_session_id=parent_session_id,
689
- agent_run_id=agent_run.id,
690
- project_id=project_id,
691
- workflow_name=workflow,
692
- agent_depth=child_session.agent_depth,
693
- max_agent_depth=runner._child_session_manager.max_agent_depth,
694
- prompt=effective_prompt,
695
- )
696
-
697
- if not headless_result.success:
698
- return {
699
- "success": False,
700
- "error": headless_result.error or headless_result.message,
701
- "run_id": agent_run.id,
702
- "child_session_id": child_session.id,
703
- }
704
-
705
- # IMPORTANT: For headless mode with -p flag, hooks are NOT called.
706
- # Claude's print mode bypasses the hook system entirely.
707
- # We must manually mark the agent run as started.
708
- try:
709
- runner._run_storage.start(agent_run.id)
710
- logger.info(f"Manually started headless agent run {agent_run.id}")
711
- except Exception as e:
712
- logger.warning(f"Failed to manually start agent run: {e}")
713
-
714
- # Register in running agents registry
715
- running_agent = RunningAgent(
716
- run_id=agent_run.id,
717
- session_id=child_session.id,
718
- parent_session_id=parent_session_id,
719
- mode="headless",
720
- pid=headless_result.pid,
721
- provider=effective_provider,
722
- workflow_name=workflow,
723
- worktree_id=worktree_id,
724
- )
725
- agent_registry.add(running_agent)
726
-
727
- # Start background task to monitor process completion
728
- import asyncio
729
-
730
- async def monitor_headless_process() -> None:
731
- """Monitor headless process and update status on completion."""
732
- try:
733
- process = headless_result.process
734
- if process is None:
735
- return
736
-
737
- # Wait for process to complete
738
- loop = asyncio.get_running_loop()
739
- return_code = await loop.run_in_executor(None, process.wait)
740
-
741
- # Capture output
742
- output = ""
743
- if process.stdout:
744
- output = process.stdout.read() or ""
745
-
746
- # Update agent run status
747
- if return_code == 0:
748
- runner._run_storage.complete(
749
- agent_run.id,
750
- result=output,
751
- tool_calls_count=0,
752
- turns_used=1,
753
- )
754
- logger.info(f"Headless agent {agent_run.id} completed successfully")
755
- else:
756
- runner._run_storage.fail(
757
- agent_run.id, error=f"Process exited with code {return_code}"
758
- )
759
- logger.warning(
760
- f"Headless agent {agent_run.id} failed with code {return_code}"
761
- )
762
-
763
- # Remove from running agents registry
764
- agent_registry.remove(agent_run.id)
765
-
766
- except Exception as e:
767
- logger.error(f"Error monitoring headless process: {e}")
768
- try:
769
- runner._run_storage.fail(agent_run.id, error=str(e))
770
- agent_registry.remove(agent_run.id)
771
- except Exception:
772
- pass # nosec B110 - best-effort cleanup during error handling
773
-
774
- # Schedule monitoring task and store reference to prevent GC
775
- running_agent.monitor_task = asyncio.create_task(monitor_headless_process())
776
-
777
- return {
778
- "success": True,
779
- "run_id": agent_run.id,
780
- "child_session_id": child_session.id,
781
- "status": "running", # Now "running" since we manually started it
782
- "message": f"Agent spawned headless (PID: {headless_result.pid})",
783
- "pid": headless_result.pid,
784
- }
785
-
786
81
  @registry.tool(
787
82
  name="get_agent_result",
788
83
  description="Get the result of a completed agent run.",
@@ -822,7 +117,7 @@ def create_agents_registry(
822
117
 
823
118
  @registry.tool(
824
119
  name="list_agents",
825
- description="List agent runs for a session.",
120
+ description="List agent runs for a session. Accepts #N, N, UUID, or prefix for session_id.",
826
121
  )
827
122
  async def list_agents(
828
123
  parent_session_id: str,
@@ -833,14 +128,20 @@ def create_agents_registry(
833
128
  List agent runs for a session.
834
129
 
835
130
  Args:
836
- parent_session_id: The parent session ID.
131
+ parent_session_id: Session reference (accepts #N, N, UUID, or prefix) for the parent.
837
132
  status: Optional status filter (pending, running, success, error, timeout, cancelled).
838
133
  limit: Maximum results (default: 20).
839
134
 
840
135
  Returns:
841
136
  Dict with list of agent runs.
842
137
  """
843
- 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)
844
145
 
845
146
  return {
846
147
  "success": True,
@@ -938,6 +239,12 @@ def create_agents_registry(
938
239
  agent = agent_registry.get(run_id)
939
240
  session_id = agent.session_id if agent else None
940
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
+
941
248
  # Kill via registry (run in thread to avoid blocking event loop)
942
249
  import asyncio
943
250
 
@@ -962,7 +269,7 @@ def create_agents_registry(
962
269
 
963
270
  @registry.tool(
964
271
  name="can_spawn_agent",
965
- 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.",
966
273
  )
967
274
  async def can_spawn_agent(parent_session_id: str) -> dict[str, Any]:
968
275
  """
@@ -971,12 +278,18 @@ def create_agents_registry(
971
278
  This checks the agent depth limit to prevent infinite nesting.
972
279
 
973
280
  Args:
974
- 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.
975
282
 
976
283
  Returns:
977
284
  Dict with can_spawn boolean and reason.
978
285
  """
979
- 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)
980
293
  return {
981
294
  "can_spawn": can_spawn,
982
295
  "reason": reason,
@@ -984,7 +297,7 @@ def create_agents_registry(
984
297
 
985
298
  @registry.tool(
986
299
  name="list_running_agents",
987
- 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.",
988
301
  )
989
302
  async def list_running_agents(
990
303
  parent_session_id: str | None = None,
@@ -997,14 +310,19 @@ def create_agents_registry(
997
310
  including PIDs and process handles not stored in the database.
998
311
 
999
312
  Args:
1000
- 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.
1001
314
  mode: Optional filter by execution mode (terminal, embedded, headless).
1002
315
 
1003
316
  Returns:
1004
317
  Dict with list of running agents.
1005
318
  """
1006
319
  if parent_session_id:
1007
- 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)
1008
326
  elif mode:
1009
327
  agents = agent_registry.list_by_mode(mode)
1010
328
  else:
@@ -1100,4 +418,22 @@ def create_agents_registry(
1100
418
  "by_parent_count": len(by_parent),
1101
419
  }
1102
420
 
421
+ # Register spawn_agent tool from spawn_agent module
422
+ from gobby.mcp_proxy.tools.spawn_agent import create_spawn_agent_registry
423
+
424
+ spawn_registry = create_spawn_agent_registry(
425
+ runner=runner,
426
+ agent_loader=agent_loader,
427
+ task_manager=task_manager,
428
+ worktree_storage=worktree_storage,
429
+ git_manager=git_manager,
430
+ clone_storage=clone_storage,
431
+ clone_manager=clone_manager,
432
+ session_manager=session_manager,
433
+ )
434
+
435
+ # Merge spawn_agent tools into agents registry
436
+ for tool_name, tool in spawn_registry._tools.items():
437
+ registry._tools[tool_name] = tool
438
+
1103
439
  return registry