shotgun-sh 0.2.17__py3-none-any.whl → 0.4.0.dev1__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 (150) hide show
  1. shotgun/agents/agent_manager.py +219 -37
  2. shotgun/agents/common.py +79 -78
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/manager.py +364 -53
  6. shotgun/agents/config/models.py +101 -21
  7. shotgun/agents/config/provider.py +51 -13
  8. shotgun/agents/config/streaming_test.py +119 -0
  9. shotgun/agents/context_analyzer/analyzer.py +6 -2
  10. shotgun/agents/conversation/__init__.py +18 -0
  11. shotgun/agents/conversation/filters.py +164 -0
  12. shotgun/agents/conversation/history/chunking.py +278 -0
  13. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  14. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  15. shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
  16. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  17. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
  18. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  19. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  20. shotgun/agents/error/__init__.py +11 -0
  21. shotgun/agents/error/models.py +19 -0
  22. shotgun/agents/export.py +12 -13
  23. shotgun/agents/models.py +66 -1
  24. shotgun/agents/plan.py +12 -13
  25. shotgun/agents/research.py +13 -10
  26. shotgun/agents/router/__init__.py +47 -0
  27. shotgun/agents/router/models.py +376 -0
  28. shotgun/agents/router/router.py +185 -0
  29. shotgun/agents/router/tools/__init__.py +18 -0
  30. shotgun/agents/router/tools/delegation_tools.py +503 -0
  31. shotgun/agents/router/tools/plan_tools.py +322 -0
  32. shotgun/agents/runner.py +230 -0
  33. shotgun/agents/specify.py +12 -13
  34. shotgun/agents/tasks.py +12 -13
  35. shotgun/agents/tools/file_management.py +49 -1
  36. shotgun/agents/tools/registry.py +2 -0
  37. shotgun/agents/tools/web_search/__init__.py +1 -2
  38. shotgun/agents/tools/web_search/gemini.py +1 -3
  39. shotgun/agents/tools/web_search/openai.py +1 -1
  40. shotgun/build_constants.py +2 -2
  41. shotgun/cli/clear.py +1 -1
  42. shotgun/cli/compact.py +5 -3
  43. shotgun/cli/context.py +44 -1
  44. shotgun/cli/error_handler.py +24 -0
  45. shotgun/cli/export.py +34 -34
  46. shotgun/cli/plan.py +34 -34
  47. shotgun/cli/research.py +17 -9
  48. shotgun/cli/spec/__init__.py +5 -0
  49. shotgun/cli/spec/backup.py +81 -0
  50. shotgun/cli/spec/commands.py +132 -0
  51. shotgun/cli/spec/models.py +48 -0
  52. shotgun/cli/spec/pull_service.py +219 -0
  53. shotgun/cli/specify.py +20 -19
  54. shotgun/cli/tasks.py +34 -34
  55. shotgun/codebase/core/change_detector.py +1 -1
  56. shotgun/codebase/core/ingestor.py +154 -8
  57. shotgun/codebase/core/manager.py +1 -1
  58. shotgun/codebase/models.py +2 -0
  59. shotgun/exceptions.py +325 -0
  60. shotgun/llm_proxy/__init__.py +17 -0
  61. shotgun/llm_proxy/client.py +215 -0
  62. shotgun/llm_proxy/models.py +137 -0
  63. shotgun/logging_config.py +42 -0
  64. shotgun/main.py +4 -0
  65. shotgun/posthog_telemetry.py +1 -1
  66. shotgun/prompts/agents/export.j2 +2 -0
  67. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
  68. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  69. shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
  70. shotgun/prompts/agents/plan.j2 +29 -1
  71. shotgun/prompts/agents/research.j2 +75 -23
  72. shotgun/prompts/agents/router.j2 +440 -0
  73. shotgun/prompts/agents/specify.j2 +80 -4
  74. shotgun/prompts/agents/state/system_state.j2 +15 -8
  75. shotgun/prompts/agents/tasks.j2 +63 -23
  76. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  77. shotgun/prompts/history/combine_summaries.j2 +53 -0
  78. shotgun/sdk/codebase.py +14 -3
  79. shotgun/settings.py +5 -0
  80. shotgun/shotgun_web/__init__.py +67 -1
  81. shotgun/shotgun_web/client.py +42 -1
  82. shotgun/shotgun_web/constants.py +46 -0
  83. shotgun/shotgun_web/exceptions.py +29 -0
  84. shotgun/shotgun_web/models.py +390 -0
  85. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  86. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  87. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  88. shotgun/shotgun_web/shared_specs/models.py +71 -0
  89. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  90. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  91. shotgun/shotgun_web/specs_client.py +703 -0
  92. shotgun/shotgun_web/supabase_client.py +31 -0
  93. shotgun/tui/app.py +78 -15
  94. shotgun/tui/components/mode_indicator.py +120 -25
  95. shotgun/tui/components/status_bar.py +2 -2
  96. shotgun/tui/containers.py +1 -1
  97. shotgun/tui/dependencies.py +64 -9
  98. shotgun/tui/layout.py +5 -0
  99. shotgun/tui/protocols.py +37 -0
  100. shotgun/tui/screens/chat/chat.tcss +9 -1
  101. shotgun/tui/screens/chat/chat_screen.py +1015 -106
  102. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
  103. shotgun/tui/screens/chat_screen/command_providers.py +13 -89
  104. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  105. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  106. shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
  107. shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
  108. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  109. shotgun/tui/screens/chat_screen/messages.py +219 -0
  110. shotgun/tui/screens/confirmation_dialog.py +40 -0
  111. shotgun/tui/screens/directory_setup.py +45 -41
  112. shotgun/tui/screens/feedback.py +10 -3
  113. shotgun/tui/screens/github_issue.py +11 -2
  114. shotgun/tui/screens/model_picker.py +28 -8
  115. shotgun/tui/screens/onboarding.py +179 -26
  116. shotgun/tui/screens/pipx_migration.py +58 -6
  117. shotgun/tui/screens/provider_config.py +66 -8
  118. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  119. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  120. shotgun/tui/screens/shared_specs/models.py +56 -0
  121. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  122. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  123. shotgun/tui/screens/shotgun_auth.py +110 -16
  124. shotgun/tui/screens/spec_pull.py +288 -0
  125. shotgun/tui/screens/welcome.py +123 -0
  126. shotgun/tui/services/conversation_service.py +5 -2
  127. shotgun/tui/utils/mode_progress.py +20 -86
  128. shotgun/tui/widgets/__init__.py +2 -1
  129. shotgun/tui/widgets/approval_widget.py +152 -0
  130. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  131. shotgun/tui/widgets/plan_panel.py +129 -0
  132. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  133. shotgun/tui/widgets/widget_coordinator.py +1 -1
  134. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
  135. shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
  136. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
  137. shotgun_sh-0.2.17.dist-info/RECORD +0 -194
  138. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  139. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  140. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  141. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  142. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  143. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  144. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  145. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  146. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  147. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  148. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  149. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
  150. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,185 @@
1
+ """Router agent factory for the intelligent orchestrator.
2
+
3
+ The Router agent is the single user-facing interface that orchestrates
4
+ sub-agents (Research, Specify, Plan, Tasks, Export) based on user intent.
5
+ """
6
+
7
+ import traceback
8
+ from functools import partial
9
+
10
+ from pydantic_ai import Agent, Tool
11
+ from pydantic_ai.agent import AgentRunResult
12
+ from pydantic_ai.messages import ModelMessage
13
+
14
+ from shotgun.agents.common import (
15
+ add_system_status_message,
16
+ build_agent_system_prompt,
17
+ create_usage_limits,
18
+ run_agent,
19
+ )
20
+ from shotgun.agents.config import ProviderType, get_provider_model
21
+ from shotgun.agents.conversation.history import token_limit_compactor
22
+ from shotgun.agents.models import AgentResponse, AgentRuntimeOptions, AgentType
23
+ from shotgun.agents.router.models import RouterDeps
24
+ from shotgun.agents.router.tools import (
25
+ add_step,
26
+ create_plan,
27
+ mark_step_done,
28
+ remove_step,
29
+ )
30
+ from shotgun.agents.router.tools.delegation_tools import (
31
+ delegate_to_export,
32
+ delegate_to_plan,
33
+ delegate_to_research,
34
+ delegate_to_specification,
35
+ delegate_to_tasks,
36
+ prepare_delegation_tool,
37
+ )
38
+ from shotgun.agents.tools import read_file
39
+ from shotgun.logging_config import get_logger
40
+ from shotgun.sdk.services import get_codebase_service
41
+ from shotgun.utils import ensure_shotgun_directory_exists
42
+
43
+ logger = get_logger(__name__)
44
+
45
+
46
+ async def create_router_agent(
47
+ agent_runtime_options: AgentRuntimeOptions,
48
+ provider: ProviderType | None = None,
49
+ ) -> tuple[Agent[RouterDeps, AgentResponse], RouterDeps]:
50
+ """Create the Router agent with plan management and delegation capabilities.
51
+
52
+ The Router is the intelligent orchestrator that:
53
+ - Understands user intent
54
+ - Creates and manages execution plans
55
+ - Delegates work to specialized sub-agents
56
+ - Operates in Planning (incremental) or Drafting (auto-execute) mode
57
+
58
+ Args:
59
+ agent_runtime_options: Runtime options for the agent
60
+ provider: Optional provider override. If None, uses configured default
61
+
62
+ Returns:
63
+ Tuple of (Configured Router agent, RouterDeps with plan management state)
64
+ """
65
+ logger.debug("Initializing router agent")
66
+ ensure_shotgun_directory_exists()
67
+
68
+ # Get configured model
69
+ try:
70
+ model_config = await get_provider_model(provider)
71
+ logger.debug(
72
+ "Router agent using %s model: %s",
73
+ model_config.provider.value.upper(),
74
+ model_config.name,
75
+ )
76
+ model = model_config.model_instance
77
+ except Exception as e:
78
+ logger.error("Failed to load configured model for router: %s", e)
79
+ raise ValueError("Configured model is required for router agent") from e
80
+
81
+ # Create RouterDeps (extends AgentDeps with router-specific state)
82
+ codebase_service = get_codebase_service()
83
+ system_prompt_fn = partial(build_agent_system_prompt, "router")
84
+
85
+ deps = RouterDeps(
86
+ **agent_runtime_options.model_dump(),
87
+ llm_model=model_config,
88
+ codebase_service=codebase_service,
89
+ system_prompt_fn=system_prompt_fn,
90
+ agent_mode=AgentType.ROUTER,
91
+ )
92
+
93
+ # Create history processor with access to deps via closure
94
+ async def history_processor(messages: list[ModelMessage]) -> list[ModelMessage]:
95
+ """History processor with access to deps via closure."""
96
+
97
+ class ProcessorContext:
98
+ def __init__(self, deps: RouterDeps):
99
+ self.deps = deps
100
+ self.usage = None
101
+
102
+ ctx = ProcessorContext(deps)
103
+ return await token_limit_compactor(ctx, messages) # type: ignore[arg-type]
104
+
105
+ # Delegation tools with prepare function - only visible after plan is approved
106
+ # in Planning mode, always available in Drafting mode
107
+ delegation_tools = [
108
+ Tool(delegate_to_research, prepare=prepare_delegation_tool),
109
+ Tool(delegate_to_specification, prepare=prepare_delegation_tool),
110
+ Tool(delegate_to_plan, prepare=prepare_delegation_tool),
111
+ Tool(delegate_to_tasks, prepare=prepare_delegation_tool),
112
+ Tool(delegate_to_export, prepare=prepare_delegation_tool),
113
+ ]
114
+
115
+ # Create the agent with delegation tools that have prepare functions
116
+ agent: Agent[RouterDeps, AgentResponse] = Agent(
117
+ model,
118
+ output_type=AgentResponse,
119
+ deps_type=RouterDeps,
120
+ instrument=True,
121
+ history_processors=[history_processor],
122
+ retries=3,
123
+ tools=delegation_tools,
124
+ )
125
+
126
+ # Register plan management tools (router-specific, always available)
127
+ agent.tool(create_plan)
128
+ agent.tool(mark_step_done)
129
+ agent.tool(add_step)
130
+ agent.tool(remove_step)
131
+
132
+ # Register read-only file access for .shotgun/ directory
133
+ agent.tool(read_file)
134
+
135
+ # Note: The Router does NOT have write_file, append_file, or codebase tools.
136
+ # All file modifications and codebase understanding must be delegated to
137
+ # the appropriate sub-agent (Research, Specify, Plan, Tasks, Export).
138
+
139
+ logger.debug("Router agent tools registered")
140
+ logger.info(
141
+ "Router agent created in %s mode",
142
+ deps.router_mode.value.upper(),
143
+ )
144
+
145
+ return agent, deps
146
+
147
+
148
+ async def run_router_agent(
149
+ agent: Agent[RouterDeps, AgentResponse],
150
+ prompt: str,
151
+ deps: RouterDeps,
152
+ message_history: list[ModelMessage] | None = None,
153
+ ) -> AgentRunResult[AgentResponse]:
154
+ """Run the router agent with a user prompt.
155
+
156
+ Args:
157
+ agent: The configured router agent
158
+ prompt: User's request
159
+ deps: RouterDeps with plan management state
160
+ message_history: Optional existing message history
161
+
162
+ Returns:
163
+ Agent run result with response and any clarifying questions
164
+ """
165
+ logger.debug("Running router agent with prompt: %s", prompt[:100])
166
+
167
+ message_history = await add_system_status_message(deps, message_history)
168
+
169
+ try:
170
+ usage_limits = create_usage_limits()
171
+
172
+ result = await run_agent(
173
+ agent=agent, # type: ignore[arg-type]
174
+ prompt=prompt,
175
+ deps=deps,
176
+ message_history=message_history,
177
+ usage_limits=usage_limits,
178
+ )
179
+
180
+ logger.debug("Router agent completed successfully")
181
+ return result
182
+
183
+ except Exception:
184
+ logger.error("Router agent error:\n%s", traceback.format_exc())
185
+ raise
@@ -0,0 +1,18 @@
1
+ """Router tools package."""
2
+
3
+ from shotgun.agents.router.tools.plan_tools import (
4
+ add_step,
5
+ create_plan,
6
+ mark_step_done,
7
+ remove_step,
8
+ )
9
+
10
+ # Note: Delegation tools are imported directly in router.py to use
11
+ # the prepare_delegation_tool function for conditional visibility.
12
+
13
+ __all__ = [
14
+ "add_step",
15
+ "create_plan",
16
+ "mark_step_done",
17
+ "remove_step",
18
+ ]
@@ -0,0 +1,503 @@
1
+ """Delegation tools for the Router agent.
2
+
3
+ These tools allow the Router to delegate work to specialized sub-agents
4
+ (Research, Specify, Plan, Tasks, Export) for specific tasks.
5
+
6
+ Sub-agents run with isolated message histories to prevent context window bloat.
7
+ """
8
+
9
+ from collections.abc import Awaitable, Callable
10
+ from typing import Any
11
+
12
+ from pydantic_ai import RunContext
13
+ from pydantic_ai.tools import ToolDefinition
14
+
15
+ from shotgun.agents.export import create_export_agent, run_export_agent
16
+ from shotgun.agents.models import (
17
+ AgentDeps,
18
+ AgentRuntimeOptions,
19
+ AgentType,
20
+ ShotgunAgent,
21
+ SubAgentContext,
22
+ )
23
+ from shotgun.agents.plan import create_plan_agent, run_plan_agent
24
+ from shotgun.agents.research import create_research_agent, run_research_agent
25
+ from shotgun.agents.router.models import (
26
+ DelegationInput,
27
+ DelegationResult,
28
+ RouterDeps,
29
+ RouterMode,
30
+ SubAgentCacheEntry,
31
+ )
32
+ from shotgun.agents.specify import create_specify_agent, run_specify_agent
33
+ from shotgun.agents.tasks import create_tasks_agent, run_tasks_agent
34
+ from shotgun.agents.tools.registry import ToolCategory, register_tool
35
+ from shotgun.logging_config import get_logger
36
+
37
+ logger = get_logger(__name__)
38
+
39
+
40
+ # =============================================================================
41
+ # Tool Preparation (Conditional Availability)
42
+ # =============================================================================
43
+
44
+
45
+ async def prepare_delegation_tool(
46
+ ctx: RunContext[RouterDeps], tool_def: ToolDefinition
47
+ ) -> ToolDefinition | None:
48
+ """Prepare function to conditionally show delegation tools.
49
+
50
+ In Planning mode, delegation tools are ONLY available when:
51
+ 1. A plan exists (current_plan is not None)
52
+ 2. The plan has been approved (pending_approval is None)
53
+
54
+ In Drafting mode, delegation tools are always available.
55
+
56
+ Args:
57
+ ctx: RunContext with RouterDeps containing plan state.
58
+ tool_def: The tool definition to conditionally return.
59
+
60
+ Returns:
61
+ The tool_def if delegation is allowed, None to hide the tool.
62
+ """
63
+ deps = ctx.deps
64
+
65
+ # Drafting mode - tools always available
66
+ if deps.router_mode == RouterMode.DRAFTING:
67
+ return tool_def
68
+
69
+ # Planning mode - check plan state
70
+ if deps.current_plan is None:
71
+ logger.debug("Hiding %s: no plan exists in Planning mode", tool_def.name)
72
+ return None
73
+
74
+ if deps.pending_approval is not None:
75
+ logger.debug("Hiding %s: plan pending user approval", tool_def.name)
76
+ return None
77
+
78
+ # Plan exists and is approved - allow delegation
79
+ return tool_def
80
+
81
+
82
+ # Type aliases for factory functions
83
+ CreateAgentFn = Callable[
84
+ [AgentRuntimeOptions], Awaitable[tuple[ShotgunAgent, AgentDeps]]
85
+ ]
86
+ RunAgentFn = Callable[..., Awaitable[Any]]
87
+
88
+ # Maximum retries for transient errors
89
+ MAX_RETRIES = 2
90
+
91
+ # Map agent types to their factory and run functions
92
+ AGENT_FACTORIES: dict[AgentType, tuple[CreateAgentFn, RunAgentFn]] = {
93
+ AgentType.RESEARCH: (create_research_agent, run_research_agent),
94
+ AgentType.SPECIFY: (create_specify_agent, run_specify_agent),
95
+ AgentType.PLAN: (create_plan_agent, run_plan_agent),
96
+ AgentType.TASKS: (create_tasks_agent, run_tasks_agent),
97
+ AgentType.EXPORT: (create_export_agent, run_export_agent),
98
+ }
99
+
100
+
101
+ def _is_retryable_error(exception: BaseException) -> bool:
102
+ """Check if exception should trigger a retry.
103
+
104
+ Args:
105
+ exception: The exception to check.
106
+
107
+ Returns:
108
+ True if the exception is a transient error that should be retried.
109
+ """
110
+ # ValueError for truncated/incomplete JSON
111
+ if isinstance(exception, ValueError):
112
+ error_str = str(exception)
113
+ return "EOF while parsing" in error_str or (
114
+ "JSON" in error_str and "parsing" in error_str
115
+ )
116
+
117
+ # API errors (overload, rate limits)
118
+ exception_name = type(exception).__name__
119
+ if "APIStatusError" in exception_name:
120
+ error_str = str(exception)
121
+ return "overload" in error_str.lower() or "rate" in error_str.lower()
122
+
123
+ # Network errors
124
+ if "ConnectionError" in exception_name or "TimeoutError" in exception_name:
125
+ return True
126
+
127
+ return False
128
+
129
+
130
+ def _create_agent_runtime_options(deps: RouterDeps) -> AgentRuntimeOptions:
131
+ """Create AgentRuntimeOptions from RouterDeps for sub-agent creation.
132
+
133
+ Args:
134
+ deps: RouterDeps containing shared runtime configuration.
135
+
136
+ Returns:
137
+ AgentRuntimeOptions configured for sub-agent creation.
138
+ """
139
+ return AgentRuntimeOptions(
140
+ interactive_mode=deps.interactive_mode,
141
+ working_directory=deps.working_directory,
142
+ is_tui_context=deps.is_tui_context,
143
+ max_iterations=deps.max_iterations,
144
+ queue=deps.queue,
145
+ tasks=deps.tasks,
146
+ )
147
+
148
+
149
+ async def _get_or_create_sub_agent(
150
+ deps: RouterDeps,
151
+ agent_type: AgentType,
152
+ ) -> SubAgentCacheEntry:
153
+ """Get a cached sub-agent or create a new one.
154
+
155
+ Args:
156
+ deps: RouterDeps with sub_agent_cache.
157
+ agent_type: The type of agent to get or create.
158
+
159
+ Returns:
160
+ Tuple of (agent, agent_deps) for the requested agent type.
161
+
162
+ Raises:
163
+ ValueError: If agent_type is not supported for delegation.
164
+ """
165
+ # Check cache first
166
+ if agent_type in deps.sub_agent_cache:
167
+ logger.debug("Using cached %s agent", agent_type.value)
168
+ return deps.sub_agent_cache[agent_type]
169
+
170
+ # Get factory functions
171
+ if agent_type not in AGENT_FACTORIES:
172
+ raise ValueError(f"Agent type {agent_type} is not supported for delegation")
173
+
174
+ create_fn, _ = AGENT_FACTORIES[agent_type]
175
+ runtime_options = _create_agent_runtime_options(deps)
176
+
177
+ logger.debug("Creating new %s agent for delegation", agent_type.value)
178
+ agent, agent_deps = await create_fn(runtime_options)
179
+
180
+ # Cache for reuse
181
+ cache_entry: SubAgentCacheEntry = (agent, agent_deps)
182
+ deps.sub_agent_cache[agent_type] = cache_entry
183
+
184
+ return cache_entry
185
+
186
+
187
+ def _build_sub_agent_context(deps: RouterDeps) -> SubAgentContext:
188
+ """Build SubAgentContext with plan information.
189
+
190
+ Args:
191
+ deps: RouterDeps with current plan.
192
+
193
+ Returns:
194
+ SubAgentContext configured for delegation.
195
+ """
196
+ current_step = deps.current_plan.current_step() if deps.current_plan else None
197
+
198
+ return SubAgentContext(
199
+ is_router_delegated=True,
200
+ plan_goal=deps.current_plan.goal if deps.current_plan else "",
201
+ current_step_id=current_step.id if current_step else "",
202
+ current_step_title=current_step.title if current_step else "",
203
+ )
204
+
205
+
206
+ async def _run_sub_agent(
207
+ ctx: RunContext[RouterDeps],
208
+ agent_type: AgentType,
209
+ task: str,
210
+ context_hint: str | None = None,
211
+ ) -> DelegationResult:
212
+ """Run a sub-agent with the given task.
213
+
214
+ This helper function handles:
215
+ - Checking for pending approval (blocks delegation until user approves)
216
+ - Getting or creating the sub-agent from cache
217
+ - Setting up SubAgentContext
218
+ - Managing active_sub_agent state for UI updates
219
+ - Running the sub-agent with isolated message history
220
+ - Extracting files_modified and handling errors with retries
221
+
222
+ Args:
223
+ ctx: RunContext with RouterDeps.
224
+ agent_type: The type of sub-agent to run.
225
+ task: The task to delegate to the sub-agent.
226
+ context_hint: Optional context to help the sub-agent.
227
+
228
+ Returns:
229
+ DelegationResult with success/failure status, response, and files_modified.
230
+ """
231
+ deps = ctx.deps
232
+
233
+ # Note: Delegation checks are now handled by prepare_delegation_tool which
234
+ # hides delegation tools entirely when delegation isn't allowed. This is a
235
+ # cleaner approach than returning errors - the LLM simply won't see the tools.
236
+
237
+ # Build the prompt with context hint if provided
238
+ prompt = task
239
+ if context_hint:
240
+ prompt = f"{task}\n\nContext: {context_hint}"
241
+
242
+ # Get or create the sub-agent
243
+ try:
244
+ agent, sub_agent_deps = await _get_or_create_sub_agent(deps, agent_type)
245
+ except ValueError as e:
246
+ return DelegationResult(
247
+ success=False,
248
+ error=str(e),
249
+ response="",
250
+ files_modified=[],
251
+ )
252
+
253
+ # Set up SubAgentContext so sub-agent knows it's being orchestrated
254
+ sub_agent_deps.sub_agent_context = _build_sub_agent_context(deps)
255
+
256
+ # Clear sub-agent's file tracker for fresh tracking
257
+ sub_agent_deps.file_tracker.clear()
258
+
259
+ # Set active_sub_agent for UI mode indicator
260
+ deps.active_sub_agent = agent_type
261
+ logger.info("Delegating to %s agent: %s", agent_type.value, task[:100])
262
+
263
+ # Get the run function for this agent type
264
+ _, run_fn = AGENT_FACTORIES[agent_type]
265
+
266
+ # Retry loop for transient errors
267
+ last_error: BaseException | None = None
268
+ for attempt in range(MAX_RETRIES + 1):
269
+ try:
270
+ # Run sub-agent with isolated message history and streaming support
271
+ result = await run_fn(
272
+ agent=agent,
273
+ prompt=prompt,
274
+ deps=sub_agent_deps,
275
+ message_history=[], # Isolated context
276
+ event_stream_handler=deps.parent_stream_handler, # Forward streaming
277
+ )
278
+
279
+ # Extract response text
280
+ response_text = ""
281
+ if result and result.output:
282
+ response_text = result.output.response
283
+
284
+ # Extract files modified
285
+ files_modified = [
286
+ op.file_path for op in sub_agent_deps.file_tracker.operations
287
+ ]
288
+
289
+ # Check for clarifying questions
290
+ has_questions = False
291
+ questions: list[str] = []
292
+ if result and result.output and result.output.clarifying_questions:
293
+ has_questions = True
294
+ questions = result.output.clarifying_questions
295
+
296
+ logger.info(
297
+ "Sub-agent %s completed. Files modified: %s",
298
+ agent_type.value,
299
+ files_modified,
300
+ )
301
+
302
+ # Clear active_sub_agent
303
+ deps.active_sub_agent = None
304
+
305
+ return DelegationResult(
306
+ success=True,
307
+ response=response_text,
308
+ files_modified=files_modified,
309
+ has_questions=has_questions,
310
+ questions=questions,
311
+ )
312
+
313
+ except Exception as e:
314
+ last_error = e
315
+ if _is_retryable_error(e) and attempt < MAX_RETRIES:
316
+ logger.warning(
317
+ "Sub-agent %s failed (attempt %d/%d), retrying: %s",
318
+ agent_type.value,
319
+ attempt + 1,
320
+ MAX_RETRIES + 1,
321
+ str(e),
322
+ )
323
+ continue
324
+
325
+ # Non-retryable error or max retries exceeded
326
+ logger.error(
327
+ "Sub-agent %s failed after %d attempts: %s",
328
+ agent_type.value,
329
+ attempt + 1,
330
+ str(e),
331
+ )
332
+ break
333
+
334
+ # Clear active_sub_agent on failure
335
+ deps.active_sub_agent = None
336
+
337
+ return DelegationResult(
338
+ success=False,
339
+ error=str(last_error) if last_error else "Unknown error",
340
+ response="",
341
+ files_modified=[],
342
+ )
343
+
344
+
345
+ # =============================================================================
346
+ # Delegation Tools
347
+ # =============================================================================
348
+
349
+
350
+ @register_tool(
351
+ category=ToolCategory.DELEGATION,
352
+ display_text="Delegating to Research agent",
353
+ key_arg="task",
354
+ )
355
+ async def delegate_to_research(
356
+ ctx: RunContext[RouterDeps],
357
+ input: DelegationInput,
358
+ ) -> DelegationResult:
359
+ """Delegate a task to the Research agent.
360
+
361
+ The Research agent specializes in:
362
+ - Finding information via web search
363
+ - Analyzing code and documentation
364
+ - Gathering background research
365
+ - Saving research findings to .shotgun/research.md
366
+
367
+ Args:
368
+ ctx: RunContext with RouterDeps.
369
+ input: DelegationInput with task and optional context_hint.
370
+
371
+ Returns:
372
+ DelegationResult with the research findings.
373
+ """
374
+ return await _run_sub_agent(
375
+ ctx,
376
+ AgentType.RESEARCH,
377
+ input.task,
378
+ input.context_hint,
379
+ )
380
+
381
+
382
+ @register_tool(
383
+ category=ToolCategory.DELEGATION,
384
+ display_text="Delegating to Specification agent",
385
+ key_arg="task",
386
+ )
387
+ async def delegate_to_specification(
388
+ ctx: RunContext[RouterDeps],
389
+ input: DelegationInput,
390
+ ) -> DelegationResult:
391
+ """Delegate a task to the Specification agent.
392
+
393
+ The Specification agent specializes in:
394
+ - Writing and updating .shotgun/specification.md
395
+ - Creating Pydantic contracts in .shotgun/contracts/
396
+ - Defining technical requirements and interfaces
397
+
398
+ Args:
399
+ ctx: RunContext with RouterDeps.
400
+ input: DelegationInput with task and optional context_hint.
401
+
402
+ Returns:
403
+ DelegationResult with the specification updates.
404
+ """
405
+ return await _run_sub_agent(
406
+ ctx,
407
+ AgentType.SPECIFY,
408
+ input.task,
409
+ input.context_hint,
410
+ )
411
+
412
+
413
+ @register_tool(
414
+ category=ToolCategory.DELEGATION,
415
+ display_text="Delegating to Plan agent",
416
+ key_arg="task",
417
+ )
418
+ async def delegate_to_plan(
419
+ ctx: RunContext[RouterDeps],
420
+ input: DelegationInput,
421
+ ) -> DelegationResult:
422
+ """Delegate a task to the Plan agent.
423
+
424
+ The Plan agent specializes in:
425
+ - Writing and updating .shotgun/plan.md
426
+ - Creating implementation plans with stages
427
+ - Defining technical approach and architecture
428
+
429
+ Args:
430
+ ctx: RunContext with RouterDeps.
431
+ input: DelegationInput with task and optional context_hint.
432
+
433
+ Returns:
434
+ DelegationResult with the plan updates.
435
+ """
436
+ return await _run_sub_agent(
437
+ ctx,
438
+ AgentType.PLAN,
439
+ input.task,
440
+ input.context_hint,
441
+ )
442
+
443
+
444
+ @register_tool(
445
+ category=ToolCategory.DELEGATION,
446
+ display_text="Delegating to Tasks agent",
447
+ key_arg="task",
448
+ )
449
+ async def delegate_to_tasks(
450
+ ctx: RunContext[RouterDeps],
451
+ input: DelegationInput,
452
+ ) -> DelegationResult:
453
+ """Delegate a task to the Tasks agent.
454
+
455
+ The Tasks agent specializes in:
456
+ - Writing and updating .shotgun/tasks.md
457
+ - Creating actionable implementation tasks
458
+ - Breaking down work into manageable items
459
+
460
+ Args:
461
+ ctx: RunContext with RouterDeps.
462
+ input: DelegationInput with task and optional context_hint.
463
+
464
+ Returns:
465
+ DelegationResult with the task list updates.
466
+ """
467
+ return await _run_sub_agent(
468
+ ctx,
469
+ AgentType.TASKS,
470
+ input.task,
471
+ input.context_hint,
472
+ )
473
+
474
+
475
+ @register_tool(
476
+ category=ToolCategory.DELEGATION,
477
+ display_text="Delegating to Export agent",
478
+ key_arg="task",
479
+ )
480
+ async def delegate_to_export(
481
+ ctx: RunContext[RouterDeps],
482
+ input: DelegationInput,
483
+ ) -> DelegationResult:
484
+ """Delegate a task to the Export agent.
485
+
486
+ The Export agent specializes in:
487
+ - Exporting artifacts and deliverables
488
+ - Generating outputs to .shotgun/export/
489
+ - Creating documentation exports
490
+
491
+ Args:
492
+ ctx: RunContext with RouterDeps.
493
+ input: DelegationInput with task and optional context_hint.
494
+
495
+ Returns:
496
+ DelegationResult with the export results.
497
+ """
498
+ return await _run_sub_agent(
499
+ ctx,
500
+ AgentType.EXPORT,
501
+ input.task,
502
+ input.context_hint,
503
+ )