shotgun-sh 0.3.3.dev1__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 (58) hide show
  1. shotgun/agents/agent_manager.py +191 -23
  2. shotgun/agents/common.py +78 -77
  3. shotgun/agents/config/manager.py +42 -1
  4. shotgun/agents/config/models.py +16 -0
  5. shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
  6. shotgun/agents/export.py +12 -13
  7. shotgun/agents/models.py +66 -1
  8. shotgun/agents/plan.py +12 -13
  9. shotgun/agents/research.py +13 -10
  10. shotgun/agents/router/__init__.py +47 -0
  11. shotgun/agents/router/models.py +376 -0
  12. shotgun/agents/router/router.py +185 -0
  13. shotgun/agents/router/tools/__init__.py +18 -0
  14. shotgun/agents/router/tools/delegation_tools.py +503 -0
  15. shotgun/agents/router/tools/plan_tools.py +322 -0
  16. shotgun/agents/specify.py +12 -13
  17. shotgun/agents/tasks.py +12 -13
  18. shotgun/agents/tools/file_management.py +49 -1
  19. shotgun/agents/tools/registry.py +2 -0
  20. shotgun/agents/tools/web_search/__init__.py +1 -2
  21. shotgun/agents/tools/web_search/gemini.py +1 -3
  22. shotgun/codebase/core/change_detector.py +1 -1
  23. shotgun/codebase/core/ingestor.py +1 -1
  24. shotgun/codebase/core/manager.py +1 -1
  25. shotgun/prompts/agents/export.j2 +2 -0
  26. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -10
  27. shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
  28. shotgun/prompts/agents/plan.j2 +24 -12
  29. shotgun/prompts/agents/research.j2 +70 -31
  30. shotgun/prompts/agents/router.j2 +440 -0
  31. shotgun/prompts/agents/specify.j2 +39 -16
  32. shotgun/prompts/agents/state/system_state.j2 +15 -6
  33. shotgun/prompts/agents/tasks.j2 +58 -34
  34. shotgun/tui/app.py +5 -6
  35. shotgun/tui/components/mode_indicator.py +120 -25
  36. shotgun/tui/components/status_bar.py +2 -2
  37. shotgun/tui/dependencies.py +64 -9
  38. shotgun/tui/protocols.py +37 -0
  39. shotgun/tui/screens/chat/chat.tcss +9 -1
  40. shotgun/tui/screens/chat/chat_screen.py +643 -11
  41. shotgun/tui/screens/chat_screen/command_providers.py +0 -87
  42. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  43. shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
  44. shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
  45. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  46. shotgun/tui/screens/chat_screen/messages.py +219 -0
  47. shotgun/tui/screens/onboarding.py +30 -26
  48. shotgun/tui/utils/mode_progress.py +20 -86
  49. shotgun/tui/widgets/__init__.py +2 -1
  50. shotgun/tui/widgets/approval_widget.py +152 -0
  51. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  52. shotgun/tui/widgets/plan_panel.py +129 -0
  53. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  54. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +3 -3
  55. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/RECORD +58 -45
  56. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +0 -0
  57. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
  58. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -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
+ )