shotgun-sh 0.2.29.dev2__py3-none-any.whl → 0.6.1.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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (161) hide show
  1. shotgun/agents/agent_manager.py +497 -30
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +90 -77
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +52 -8
  6. shotgun/agents/config/models.py +48 -45
  7. shotgun/agents/config/provider.py +44 -29
  8. shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
  9. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  10. shotgun/agents/export.py +12 -13
  11. shotgun/agents/file_read.py +176 -0
  12. shotgun/agents/messages.py +15 -3
  13. shotgun/agents/models.py +90 -2
  14. shotgun/agents/plan.py +12 -13
  15. shotgun/agents/research.py +13 -10
  16. shotgun/agents/router/__init__.py +47 -0
  17. shotgun/agents/router/models.py +384 -0
  18. shotgun/agents/router/router.py +185 -0
  19. shotgun/agents/router/tools/__init__.py +18 -0
  20. shotgun/agents/router/tools/delegation_tools.py +557 -0
  21. shotgun/agents/router/tools/plan_tools.py +403 -0
  22. shotgun/agents/runner.py +17 -2
  23. shotgun/agents/specify.py +12 -13
  24. shotgun/agents/tasks.py +12 -13
  25. shotgun/agents/tools/__init__.py +8 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  27. shotgun/agents/tools/codebase/file_read.py +26 -35
  28. shotgun/agents/tools/codebase/query_graph.py +9 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  30. shotgun/agents/tools/file_management.py +81 -3
  31. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  32. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  33. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  34. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  35. shotgun/agents/tools/markdown_tools/models.py +86 -0
  36. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  37. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  38. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  39. shotgun/agents/tools/registry.py +41 -0
  40. shotgun/agents/tools/web_search/__init__.py +1 -2
  41. shotgun/agents/tools/web_search/gemini.py +1 -3
  42. shotgun/agents/tools/web_search/openai.py +42 -23
  43. shotgun/attachments/__init__.py +41 -0
  44. shotgun/attachments/errors.py +60 -0
  45. shotgun/attachments/models.py +107 -0
  46. shotgun/attachments/parser.py +257 -0
  47. shotgun/attachments/processor.py +193 -0
  48. shotgun/cli/clear.py +2 -2
  49. shotgun/cli/codebase/commands.py +181 -65
  50. shotgun/cli/compact.py +2 -2
  51. shotgun/cli/context.py +2 -2
  52. shotgun/cli/run.py +90 -0
  53. shotgun/cli/spec/backup.py +2 -1
  54. shotgun/cli/spec/commands.py +2 -0
  55. shotgun/cli/spec/models.py +18 -0
  56. shotgun/cli/spec/pull_service.py +122 -68
  57. shotgun/codebase/__init__.py +2 -0
  58. shotgun/codebase/benchmarks/__init__.py +35 -0
  59. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  60. shotgun/codebase/benchmarks/exporters.py +119 -0
  61. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  62. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  63. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  64. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  65. shotgun/codebase/benchmarks/models.py +129 -0
  66. shotgun/codebase/core/__init__.py +4 -0
  67. shotgun/codebase/core/call_resolution.py +91 -0
  68. shotgun/codebase/core/change_detector.py +11 -6
  69. shotgun/codebase/core/errors.py +159 -0
  70. shotgun/codebase/core/extractors/__init__.py +23 -0
  71. shotgun/codebase/core/extractors/base.py +138 -0
  72. shotgun/codebase/core/extractors/factory.py +63 -0
  73. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  74. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  75. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  76. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  77. shotgun/codebase/core/extractors/protocol.py +109 -0
  78. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  79. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  80. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  81. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  82. shotgun/codebase/core/extractors/types.py +15 -0
  83. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  84. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  85. shotgun/codebase/core/gitignore.py +252 -0
  86. shotgun/codebase/core/ingestor.py +644 -354
  87. shotgun/codebase/core/kuzu_compat.py +119 -0
  88. shotgun/codebase/core/language_config.py +239 -0
  89. shotgun/codebase/core/manager.py +256 -46
  90. shotgun/codebase/core/metrics_collector.py +310 -0
  91. shotgun/codebase/core/metrics_types.py +347 -0
  92. shotgun/codebase/core/parallel_executor.py +424 -0
  93. shotgun/codebase/core/work_distributor.py +254 -0
  94. shotgun/codebase/core/worker.py +768 -0
  95. shotgun/codebase/indexing_state.py +86 -0
  96. shotgun/codebase/models.py +94 -0
  97. shotgun/codebase/service.py +13 -0
  98. shotgun/exceptions.py +1 -1
  99. shotgun/main.py +2 -10
  100. shotgun/prompts/agents/export.j2 +2 -0
  101. shotgun/prompts/agents/file_read.j2 +48 -0
  102. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
  103. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  104. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  105. shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
  106. shotgun/prompts/agents/plan.j2 +43 -1
  107. shotgun/prompts/agents/research.j2 +75 -20
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +94 -4
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -15
  112. shotgun/prompts/agents/tasks.j2 +77 -23
  113. shotgun/settings.py +44 -0
  114. shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
  115. shotgun/tui/app.py +90 -23
  116. shotgun/tui/commands/__init__.py +9 -1
  117. shotgun/tui/components/attachment_bar.py +87 -0
  118. shotgun/tui/components/mode_indicator.py +120 -25
  119. shotgun/tui/components/prompt_input.py +23 -28
  120. shotgun/tui/components/status_bar.py +5 -4
  121. shotgun/tui/dependencies.py +58 -8
  122. shotgun/tui/protocols.py +37 -0
  123. shotgun/tui/screens/chat/chat.tcss +24 -1
  124. shotgun/tui/screens/chat/chat_screen.py +1374 -211
  125. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  126. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  128. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  129. shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
  130. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  131. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  132. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  133. shotgun/tui/screens/chat_screen/messages.py +219 -0
  134. shotgun/tui/screens/database_locked_dialog.py +219 -0
  135. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  136. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  137. shotgun/tui/screens/model_picker.py +14 -9
  138. shotgun/tui/screens/models.py +11 -0
  139. shotgun/tui/screens/shotgun_auth.py +50 -0
  140. shotgun/tui/screens/spec_pull.py +2 -0
  141. shotgun/tui/state/processing_state.py +19 -0
  142. shotgun/tui/utils/mode_progress.py +20 -86
  143. shotgun/tui/widgets/__init__.py +2 -1
  144. shotgun/tui/widgets/approval_widget.py +152 -0
  145. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  146. shotgun/tui/widgets/plan_panel.py +129 -0
  147. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  148. shotgun/tui/widgets/widget_coordinator.py +18 -0
  149. shotgun/utils/file_system_utils.py +4 -1
  150. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
  151. shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
  152. shotgun/cli/export.py +0 -81
  153. shotgun/cli/plan.py +0 -73
  154. shotgun/cli/research.py +0 -93
  155. shotgun/cli/specify.py +0 -70
  156. shotgun/cli/tasks.py +0 -78
  157. shotgun/tui/screens/onboarding.py +0 -580
  158. shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
  159. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
  160. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
  161. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,557 @@
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
+ import time
10
+ from collections.abc import Awaitable, Callable
11
+ from typing import Any
12
+
13
+ from pydantic_ai import RunContext
14
+ from pydantic_ai.tools import ToolDefinition
15
+
16
+ from shotgun.agents.export import create_export_agent, run_export_agent
17
+ from shotgun.agents.models import (
18
+ AgentDeps,
19
+ AgentRuntimeOptions,
20
+ AgentType,
21
+ ShotgunAgent,
22
+ SubAgentContext,
23
+ )
24
+ from shotgun.agents.plan import create_plan_agent, run_plan_agent
25
+ from shotgun.agents.research import create_research_agent, run_research_agent
26
+ from shotgun.agents.router.models import (
27
+ DelegationInput,
28
+ DelegationResult,
29
+ RouterDeps,
30
+ RouterMode,
31
+ SubAgentCacheEntry,
32
+ )
33
+ from shotgun.agents.specify import create_specify_agent, run_specify_agent
34
+ from shotgun.agents.tasks import create_tasks_agent, run_tasks_agent
35
+ from shotgun.agents.tools.registry import ToolCategory, register_tool
36
+ from shotgun.logging_config import get_logger
37
+ from shotgun.posthog_telemetry import track_event
38
+
39
+ logger = get_logger(__name__)
40
+
41
+
42
+ # =============================================================================
43
+ # Tool Preparation (Conditional Availability)
44
+ # =============================================================================
45
+
46
+
47
+ async def prepare_delegation_tool(
48
+ ctx: RunContext[RouterDeps], tool_def: ToolDefinition
49
+ ) -> ToolDefinition | None:
50
+ """Prepare function to conditionally show delegation tools.
51
+
52
+ In Planning mode, delegation tools are ONLY available when:
53
+ 1. A plan exists (current_plan is not None)
54
+ 2. The plan has been approved (pending_approval is None)
55
+
56
+ In Drafting mode, delegation tools are always available.
57
+
58
+ Args:
59
+ ctx: RunContext with RouterDeps containing plan state.
60
+ tool_def: The tool definition to conditionally return.
61
+
62
+ Returns:
63
+ The tool_def if delegation is allowed, None to hide the tool.
64
+ """
65
+ deps = ctx.deps
66
+
67
+ # Drafting mode - tools always available
68
+ if deps.router_mode == RouterMode.DRAFTING:
69
+ return tool_def
70
+
71
+ # Planning mode - check plan state
72
+ if deps.current_plan is None:
73
+ logger.debug("Hiding %s: no plan exists in Planning mode", tool_def.name)
74
+ return None
75
+
76
+ if deps.pending_approval is not None:
77
+ logger.debug("Hiding %s: plan pending user approval", tool_def.name)
78
+ return None
79
+
80
+ # Plan exists and is approved - allow delegation
81
+ return tool_def
82
+
83
+
84
+ # Type aliases for factory functions
85
+ CreateAgentFn = Callable[
86
+ [AgentRuntimeOptions], Awaitable[tuple[ShotgunAgent, AgentDeps]]
87
+ ]
88
+ RunAgentFn = Callable[..., Awaitable[Any]]
89
+
90
+ # Maximum retries for transient errors
91
+ MAX_RETRIES = 2
92
+
93
+ # Map agent types to their factory and run functions
94
+ AGENT_FACTORIES: dict[AgentType, tuple[CreateAgentFn, RunAgentFn]] = {
95
+ AgentType.RESEARCH: (create_research_agent, run_research_agent),
96
+ AgentType.SPECIFY: (create_specify_agent, run_specify_agent),
97
+ AgentType.PLAN: (create_plan_agent, run_plan_agent),
98
+ AgentType.TASKS: (create_tasks_agent, run_tasks_agent),
99
+ AgentType.EXPORT: (create_export_agent, run_export_agent),
100
+ }
101
+
102
+
103
+ def _is_retryable_error(exception: BaseException) -> bool:
104
+ """Check if exception should trigger a retry.
105
+
106
+ Args:
107
+ exception: The exception to check.
108
+
109
+ Returns:
110
+ True if the exception is a transient error that should be retried.
111
+ """
112
+ # ValueError for truncated/incomplete JSON
113
+ if isinstance(exception, ValueError):
114
+ error_str = str(exception)
115
+ return "EOF while parsing" in error_str or (
116
+ "JSON" in error_str and "parsing" in error_str
117
+ )
118
+
119
+ # API errors (overload, rate limits)
120
+ exception_name = type(exception).__name__
121
+ if "APIStatusError" in exception_name:
122
+ error_str = str(exception)
123
+ return "overload" in error_str.lower() or "rate" in error_str.lower()
124
+
125
+ # Network errors
126
+ if "ConnectionError" in exception_name or "TimeoutError" in exception_name:
127
+ return True
128
+
129
+ return False
130
+
131
+
132
+ def _create_agent_runtime_options(deps: RouterDeps) -> AgentRuntimeOptions:
133
+ """Create AgentRuntimeOptions from RouterDeps for sub-agent creation.
134
+
135
+ Args:
136
+ deps: RouterDeps containing shared runtime configuration.
137
+
138
+ Returns:
139
+ AgentRuntimeOptions configured for sub-agent creation.
140
+ """
141
+ return AgentRuntimeOptions(
142
+ interactive_mode=deps.interactive_mode,
143
+ working_directory=deps.working_directory,
144
+ is_tui_context=deps.is_tui_context,
145
+ max_iterations=deps.max_iterations,
146
+ queue=deps.queue,
147
+ tasks=deps.tasks,
148
+ )
149
+
150
+
151
+ async def _get_or_create_sub_agent(
152
+ deps: RouterDeps,
153
+ agent_type: AgentType,
154
+ ) -> SubAgentCacheEntry:
155
+ """Get a cached sub-agent or create a new one.
156
+
157
+ Args:
158
+ deps: RouterDeps with sub_agent_cache.
159
+ agent_type: The type of agent to get or create.
160
+
161
+ Returns:
162
+ Tuple of (agent, agent_deps) for the requested agent type.
163
+
164
+ Raises:
165
+ ValueError: If agent_type is not supported for delegation.
166
+ """
167
+ # Check cache first
168
+ if agent_type in deps.sub_agent_cache:
169
+ logger.debug("Using cached %s agent", agent_type.value)
170
+ return deps.sub_agent_cache[agent_type]
171
+
172
+ # Get factory functions
173
+ if agent_type not in AGENT_FACTORIES:
174
+ raise ValueError(f"Agent type {agent_type} is not supported for delegation")
175
+
176
+ create_fn, _ = AGENT_FACTORIES[agent_type]
177
+ runtime_options = _create_agent_runtime_options(deps)
178
+
179
+ logger.debug("Creating new %s agent for delegation", agent_type.value)
180
+ agent, agent_deps = await create_fn(runtime_options)
181
+
182
+ # Cache for reuse
183
+ cache_entry: SubAgentCacheEntry = (agent, agent_deps)
184
+ deps.sub_agent_cache[agent_type] = cache_entry
185
+
186
+ return cache_entry
187
+
188
+
189
+ def _build_sub_agent_context(deps: RouterDeps) -> SubAgentContext:
190
+ """Build SubAgentContext with plan information.
191
+
192
+ Args:
193
+ deps: RouterDeps with current plan.
194
+
195
+ Returns:
196
+ SubAgentContext configured for delegation.
197
+ """
198
+ current_step = deps.current_plan.current_step() if deps.current_plan else None
199
+
200
+ return SubAgentContext(
201
+ is_router_delegated=True,
202
+ plan_goal=deps.current_plan.goal if deps.current_plan else "",
203
+ current_step_id=current_step.id if current_step else "",
204
+ current_step_title=current_step.title if current_step else "",
205
+ )
206
+
207
+
208
+ async def _run_sub_agent(
209
+ ctx: RunContext[RouterDeps],
210
+ agent_type: AgentType,
211
+ task: str,
212
+ context_hint: str | None = None,
213
+ ) -> DelegationResult:
214
+ """Run a sub-agent with the given task.
215
+
216
+ This helper function handles:
217
+ - Checking for pending approval (blocks delegation until user approves)
218
+ - Getting or creating the sub-agent from cache
219
+ - Setting up SubAgentContext
220
+ - Managing active_sub_agent state for UI updates
221
+ - Running the sub-agent with isolated message history
222
+ - Extracting files_modified and handling errors with retries
223
+
224
+ Args:
225
+ ctx: RunContext with RouterDeps.
226
+ agent_type: The type of sub-agent to run.
227
+ task: The task to delegate to the sub-agent.
228
+ context_hint: Optional context to help the sub-agent.
229
+
230
+ Returns:
231
+ DelegationResult with success/failure status, response, and files_modified.
232
+ """
233
+ deps = ctx.deps
234
+
235
+ # Note: Delegation checks are now handled by prepare_delegation_tool which
236
+ # hides delegation tools entirely when delegation isn't allowed. This is a
237
+ # cleaner approach than returning errors - the LLM simply won't see the tools.
238
+
239
+ # Build the prompt with context hint if provided
240
+ prompt = task
241
+ if context_hint:
242
+ prompt = f"{task}\n\nContext: {context_hint}"
243
+
244
+ # Get or create the sub-agent
245
+ try:
246
+ agent, sub_agent_deps = await _get_or_create_sub_agent(deps, agent_type)
247
+ except ValueError as e:
248
+ return DelegationResult(
249
+ success=False,
250
+ error=str(e),
251
+ response="",
252
+ files_modified=[],
253
+ )
254
+
255
+ # Set up SubAgentContext so sub-agent knows it's being orchestrated
256
+ sub_agent_deps.sub_agent_context = _build_sub_agent_context(deps)
257
+
258
+ # Propagate cancellation event for responsive ESC handling in sub-agents
259
+ sub_agent_deps.cancellation_event = deps.cancellation_event
260
+
261
+ # Clear sub-agent's file tracker for fresh tracking
262
+ sub_agent_deps.file_tracker.clear()
263
+
264
+ # Set active_sub_agent for UI mode indicator
265
+ deps.active_sub_agent = agent_type
266
+ logger.info("Delegating to %s agent: %s", agent_type.value, task[:100])
267
+
268
+ # Track delegation start time and event
269
+ start_time = time.time()
270
+ track_event(
271
+ "delegation_started",
272
+ {
273
+ "target_agent": agent_type.value,
274
+ "task_length": len(task),
275
+ "has_context_hint": context_hint is not None,
276
+ },
277
+ )
278
+
279
+ # Get the run function for this agent type
280
+ _, run_fn = AGENT_FACTORIES[agent_type]
281
+
282
+ # Retry loop for transient errors
283
+ last_error: BaseException | None = None
284
+ retries_attempted = 0
285
+ for attempt in range(MAX_RETRIES + 1):
286
+ try:
287
+ # Run sub-agent with isolated message history and streaming support
288
+ result = await run_fn(
289
+ agent=agent,
290
+ prompt=prompt,
291
+ deps=sub_agent_deps,
292
+ message_history=[], # Isolated context
293
+ event_stream_handler=deps.parent_stream_handler, # Forward streaming
294
+ )
295
+
296
+ # Extract response text
297
+ response_text = ""
298
+ if result and result.output:
299
+ response_text = result.output.response
300
+
301
+ # Extract files modified
302
+ files_modified = [
303
+ op.file_path for op in sub_agent_deps.file_tracker.operations
304
+ ]
305
+
306
+ # Extract files found (used by FileReadAgent)
307
+ files_found: list[str] = []
308
+ if result and result.output and result.output.files_found:
309
+ files_found = result.output.files_found
310
+
311
+ # Check for clarifying questions
312
+ has_questions = False
313
+ questions: list[str] = []
314
+ if result and result.output and result.output.clarifying_questions:
315
+ has_questions = True
316
+ questions = result.output.clarifying_questions
317
+
318
+ logger.info(
319
+ "Sub-agent %s completed. Files modified: %s, files found: %s",
320
+ agent_type.value,
321
+ files_modified,
322
+ files_found,
323
+ )
324
+
325
+ # Track delegation completion metric
326
+ track_event(
327
+ "delegation_completed",
328
+ {
329
+ "target_agent": agent_type.value,
330
+ "files_modified_count": len(files_modified),
331
+ "has_questions": has_questions,
332
+ "duration_seconds": round(time.time() - start_time, 2),
333
+ },
334
+ )
335
+
336
+ # Clear active_sub_agent
337
+ deps.active_sub_agent = None
338
+
339
+ return DelegationResult(
340
+ success=True,
341
+ response=response_text,
342
+ files_modified=files_modified,
343
+ files_found=files_found,
344
+ has_questions=has_questions,
345
+ questions=questions,
346
+ )
347
+
348
+ except Exception as e:
349
+ last_error = e
350
+ retries_attempted = attempt
351
+ if _is_retryable_error(e) and attempt < MAX_RETRIES:
352
+ logger.warning(
353
+ "Sub-agent %s failed (attempt %d/%d), retrying: %s",
354
+ agent_type.value,
355
+ attempt + 1,
356
+ MAX_RETRIES + 1,
357
+ str(e),
358
+ )
359
+ continue
360
+
361
+ # Non-retryable error or max retries exceeded
362
+ logger.error(
363
+ "Sub-agent %s failed after %d attempts: %s",
364
+ agent_type.value,
365
+ attempt + 1,
366
+ str(e),
367
+ )
368
+ # Clear the agent from cache on failure so next request gets a fresh agent
369
+ # This is especially important for request_limit errors
370
+ if agent_type in deps.sub_agent_cache:
371
+ del deps.sub_agent_cache[agent_type]
372
+ logger.debug(
373
+ "Cleared %s from sub_agent_cache after failure", agent_type.value
374
+ )
375
+ break
376
+
377
+ # Track delegation failure metric
378
+ track_event(
379
+ "delegation_failed",
380
+ {
381
+ "target_agent": agent_type.value,
382
+ "error_type": type(last_error).__name__ if last_error else "Unknown",
383
+ "retries_attempted": retries_attempted,
384
+ "duration_seconds": round(time.time() - start_time, 2),
385
+ },
386
+ )
387
+
388
+ # Clear active_sub_agent on failure
389
+ deps.active_sub_agent = None
390
+
391
+ return DelegationResult(
392
+ success=False,
393
+ error=str(last_error) if last_error else "Unknown error",
394
+ response="",
395
+ files_modified=[],
396
+ )
397
+
398
+
399
+ # =============================================================================
400
+ # Delegation Tools
401
+ # =============================================================================
402
+
403
+
404
+ @register_tool(
405
+ category=ToolCategory.DELEGATION,
406
+ display_text="Delegating to Research agent",
407
+ key_arg="task",
408
+ )
409
+ async def delegate_to_research(
410
+ ctx: RunContext[RouterDeps],
411
+ input: DelegationInput,
412
+ ) -> DelegationResult:
413
+ """Delegate a task to the Research agent.
414
+
415
+ The Research agent specializes in:
416
+ - Finding information via web search
417
+ - Analyzing code and documentation
418
+ - Gathering background research
419
+ - Saving research findings to .shotgun/research.md
420
+
421
+ Args:
422
+ ctx: RunContext with RouterDeps.
423
+ input: DelegationInput with task and optional context_hint.
424
+
425
+ Returns:
426
+ DelegationResult with the research findings.
427
+ """
428
+ return await _run_sub_agent(
429
+ ctx,
430
+ AgentType.RESEARCH,
431
+ input.task,
432
+ input.context_hint,
433
+ )
434
+
435
+
436
+ @register_tool(
437
+ category=ToolCategory.DELEGATION,
438
+ display_text="Delegating to Specification agent",
439
+ key_arg="task",
440
+ )
441
+ async def delegate_to_specification(
442
+ ctx: RunContext[RouterDeps],
443
+ input: DelegationInput,
444
+ ) -> DelegationResult:
445
+ """Delegate a task to the Specification agent.
446
+
447
+ The Specification agent specializes in:
448
+ - Writing and updating .shotgun/specification.md
449
+ - Creating Pydantic contracts in .shotgun/contracts/
450
+ - Defining technical requirements and interfaces
451
+
452
+ Args:
453
+ ctx: RunContext with RouterDeps.
454
+ input: DelegationInput with task and optional context_hint.
455
+
456
+ Returns:
457
+ DelegationResult with the specification updates.
458
+ """
459
+ return await _run_sub_agent(
460
+ ctx,
461
+ AgentType.SPECIFY,
462
+ input.task,
463
+ input.context_hint,
464
+ )
465
+
466
+
467
+ @register_tool(
468
+ category=ToolCategory.DELEGATION,
469
+ display_text="Delegating to Plan agent",
470
+ key_arg="task",
471
+ )
472
+ async def delegate_to_plan(
473
+ ctx: RunContext[RouterDeps],
474
+ input: DelegationInput,
475
+ ) -> DelegationResult:
476
+ """Delegate a task to the Plan agent.
477
+
478
+ The Plan agent specializes in:
479
+ - Writing and updating .shotgun/plan.md
480
+ - Creating implementation plans with stages
481
+ - Defining technical approach and architecture
482
+
483
+ Args:
484
+ ctx: RunContext with RouterDeps.
485
+ input: DelegationInput with task and optional context_hint.
486
+
487
+ Returns:
488
+ DelegationResult with the plan updates.
489
+ """
490
+ return await _run_sub_agent(
491
+ ctx,
492
+ AgentType.PLAN,
493
+ input.task,
494
+ input.context_hint,
495
+ )
496
+
497
+
498
+ @register_tool(
499
+ category=ToolCategory.DELEGATION,
500
+ display_text="Delegating to Tasks agent",
501
+ key_arg="task",
502
+ )
503
+ async def delegate_to_tasks(
504
+ ctx: RunContext[RouterDeps],
505
+ input: DelegationInput,
506
+ ) -> DelegationResult:
507
+ """Delegate a task to the Tasks agent.
508
+
509
+ The Tasks agent specializes in:
510
+ - Writing and updating .shotgun/tasks.md
511
+ - Creating actionable implementation tasks
512
+ - Breaking down work into manageable items
513
+
514
+ Args:
515
+ ctx: RunContext with RouterDeps.
516
+ input: DelegationInput with task and optional context_hint.
517
+
518
+ Returns:
519
+ DelegationResult with the task list updates.
520
+ """
521
+ return await _run_sub_agent(
522
+ ctx,
523
+ AgentType.TASKS,
524
+ input.task,
525
+ input.context_hint,
526
+ )
527
+
528
+
529
+ @register_tool(
530
+ category=ToolCategory.DELEGATION,
531
+ display_text="Delegating to Export agent",
532
+ key_arg="task",
533
+ )
534
+ async def delegate_to_export(
535
+ ctx: RunContext[RouterDeps],
536
+ input: DelegationInput,
537
+ ) -> DelegationResult:
538
+ """Delegate a task to the Export agent.
539
+
540
+ The Export agent specializes in:
541
+ - Exporting artifacts and deliverables
542
+ - Generating outputs to .shotgun/export/
543
+ - Creating documentation exports
544
+
545
+ Args:
546
+ ctx: RunContext with RouterDeps.
547
+ input: DelegationInput with task and optional context_hint.
548
+
549
+ Returns:
550
+ DelegationResult with the export results.
551
+ """
552
+ return await _run_sub_agent(
553
+ ctx,
554
+ AgentType.EXPORT,
555
+ input.task,
556
+ input.context_hint,
557
+ )