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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +96 -35
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/adapters/gemini.py +140 -38
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +525 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +415 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/macos.py +26 -1
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/memory.py +185 -0
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/clones/git.py +177 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +150 -8
- gobby/hooks/hook_manager.py +21 -3
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +4 -2
- gobby/mcp_proxy/registries.py +22 -8
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +76 -740
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +455 -0
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -350
- gobby/memory/extractor.py +15 -1
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +13 -0
- gobby/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +51 -4
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/manager.py +9 -0
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +30 -2
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +174 -368
- gobby/storage/sessions.py +45 -7
- gobby/storage/skills.py +80 -7
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +281 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/enforcement/task_policy.py +542 -0
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +91 -0
- gobby/workflows/safe_evaluator.py +191 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +217 -51
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/cli/tui.py +0 -34
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/mcp_proxy/tools/session_messages.py +0 -1055
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- gobby/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
gobby/mcp_proxy/tools/agents.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Internal MCP tools for Gobby Agent System.
|
|
3
3
|
|
|
4
4
|
Exposes functionality for:
|
|
5
|
-
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|