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
shotgun/agents/plan.py CHANGED
@@ -2,16 +2,15 @@
2
2
 
3
3
  from functools import partial
4
4
 
5
- from pydantic_ai import (
6
- Agent,
7
- )
8
5
  from pydantic_ai.agent import AgentRunResult
9
6
  from pydantic_ai.messages import ModelMessage
10
7
 
11
8
  from shotgun.agents.config import ProviderType
9
+ from shotgun.agents.models import ShotgunAgent
12
10
  from shotgun.logging_config import get_logger
13
11
 
14
12
  from .common import (
13
+ EventStreamHandler,
15
14
  add_system_status_message,
16
15
  build_agent_system_prompt,
17
16
  create_base_agent,
@@ -25,7 +24,7 @@ logger = get_logger(__name__)
25
24
 
26
25
  async def create_plan_agent(
27
26
  agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
28
- ) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
27
+ ) -> tuple[ShotgunAgent, AgentDeps]:
29
28
  """Create a plan agent with artifact management capabilities.
30
29
 
31
30
  Args:
@@ -51,26 +50,25 @@ async def create_plan_agent(
51
50
 
52
51
 
53
52
  async def run_plan_agent(
54
- agent: Agent[AgentDeps, AgentResponse],
55
- goal: str,
53
+ agent: ShotgunAgent,
54
+ prompt: str,
56
55
  deps: AgentDeps,
57
56
  message_history: list[ModelMessage] | None = None,
57
+ event_stream_handler: EventStreamHandler | None = None,
58
58
  ) -> AgentRunResult[AgentResponse]:
59
- """Create or update a plan based on the given goal using artifacts.
59
+ """Create or update a plan based on the given prompt using artifacts.
60
60
 
61
61
  Args:
62
62
  agent: The configured plan agent
63
- goal: The planning goal or instruction
63
+ prompt: The planning prompt or instruction
64
64
  deps: Agent dependencies
65
65
  message_history: Optional message history for conversation continuity
66
+ event_stream_handler: Optional callback for streaming events
66
67
 
67
68
  Returns:
68
69
  AgentRunResult containing the planning process output
69
70
  """
70
- logger.debug("📋 Starting planning for goal: %s", goal)
71
-
72
- # Simple prompt - the agent system prompt has all the artifact instructions
73
- full_prompt = f"Create a comprehensive plan for: {goal}"
71
+ logger.debug("📋 Starting planning for prompt: %s", prompt)
74
72
 
75
73
  try:
76
74
  # Create usage limits for responsible API usage
@@ -80,10 +78,11 @@ async def run_plan_agent(
80
78
 
81
79
  result = await run_agent(
82
80
  agent=agent,
83
- prompt=full_prompt,
81
+ prompt=prompt,
84
82
  deps=deps,
85
83
  message_history=message_history,
86
84
  usage_limits=usage_limits,
85
+ event_stream_handler=event_stream_handler,
87
86
  )
88
87
 
89
88
  logger.debug("✅ Planning completed successfully")
@@ -2,18 +2,17 @@
2
2
 
3
3
  from functools import partial
4
4
 
5
- from pydantic_ai import (
6
- Agent,
7
- )
8
5
  from pydantic_ai.agent import AgentRunResult
9
6
  from pydantic_ai.messages import (
10
7
  ModelMessage,
11
8
  )
12
9
 
13
10
  from shotgun.agents.config import ProviderType
11
+ from shotgun.agents.models import ShotgunAgent
14
12
  from shotgun.logging_config import get_logger
15
13
 
16
14
  from .common import (
15
+ EventStreamHandler,
17
16
  add_system_status_message,
18
17
  build_agent_system_prompt,
19
18
  create_base_agent,
@@ -28,7 +27,7 @@ logger = get_logger(__name__)
28
27
 
29
28
  async def create_research_agent(
30
29
  agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
31
- ) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
30
+ ) -> tuple[ShotgunAgent, AgentDeps]:
32
31
  """Create a research agent with web search and artifact management capabilities.
33
32
 
34
33
  Args:
@@ -65,22 +64,25 @@ async def create_research_agent(
65
64
 
66
65
 
67
66
  async def run_research_agent(
68
- agent: Agent[AgentDeps, AgentResponse],
69
- query: str,
67
+ agent: ShotgunAgent,
68
+ prompt: str,
70
69
  deps: AgentDeps,
71
70
  message_history: list[ModelMessage] | None = None,
71
+ event_stream_handler: EventStreamHandler | None = None,
72
72
  ) -> AgentRunResult[AgentResponse]:
73
- """Perform research on the given query and update research artifacts.
73
+ """Perform research on the given prompt and update research artifacts.
74
74
 
75
75
  Args:
76
76
  agent: The configured research agent
77
- query: The research query to investigate
77
+ prompt: The research prompt to investigate
78
78
  deps: Agent dependencies
79
+ message_history: Optional message history for conversation continuity
80
+ event_stream_handler: Optional callback for streaming events
79
81
 
80
82
  Returns:
81
83
  Summary of research findings
82
84
  """
83
- logger.debug("🔬 Starting research for query: %s", query)
85
+ logger.debug("🔬 Starting research for prompt: %s", prompt)
84
86
 
85
87
  message_history = await add_system_status_message(deps, message_history)
86
88
 
@@ -90,10 +92,11 @@ async def run_research_agent(
90
92
 
91
93
  result = await run_agent(
92
94
  agent=agent,
93
- prompt=query,
95
+ prompt=prompt,
94
96
  deps=deps,
95
97
  message_history=message_history,
96
98
  usage_limits=usage_limits,
99
+ event_stream_handler=event_stream_handler,
97
100
  )
98
101
 
99
102
  logger.debug("✅ Research completed successfully")
@@ -0,0 +1,47 @@
1
+ """Router Agent - The intelligent orchestrator for shotgun agents."""
2
+
3
+ from shotgun.agents.router.models import (
4
+ CascadeScope,
5
+ CreatePlanInput,
6
+ DelegationInput,
7
+ DelegationResult,
8
+ ExecutionPlan,
9
+ ExecutionStep,
10
+ ExecutionStepInput,
11
+ MarkStepDoneInput,
12
+ PlanApprovalStatus,
13
+ RemoveStepInput,
14
+ RouterDeps,
15
+ RouterMode,
16
+ StepCheckpointAction,
17
+ SubAgentResult,
18
+ SubAgentResultStatus,
19
+ ToolResult,
20
+ )
21
+ from shotgun.agents.router.router import create_router_agent, run_router_agent
22
+
23
+ __all__ = [
24
+ # Agent factory
25
+ "create_router_agent",
26
+ "run_router_agent",
27
+ # Enums
28
+ "RouterMode",
29
+ "PlanApprovalStatus",
30
+ "StepCheckpointAction",
31
+ "CascadeScope",
32
+ "SubAgentResultStatus",
33
+ # Plan models
34
+ "ExecutionStep",
35
+ "ExecutionPlan",
36
+ # Tool I/O models
37
+ "ExecutionStepInput",
38
+ "CreatePlanInput",
39
+ "MarkStepDoneInput",
40
+ "RemoveStepInput",
41
+ "DelegationInput",
42
+ "ToolResult",
43
+ "DelegationResult",
44
+ "SubAgentResult",
45
+ # Deps
46
+ "RouterDeps",
47
+ ]
@@ -0,0 +1,376 @@
1
+ """
2
+ Router Agent Data Models.
3
+
4
+ Type definitions for the Router Agent MVP.
5
+ These models define the contracts between router, sub-agents, and UI.
6
+
7
+ IMPORTANT: All tool inputs/outputs must use Pydantic models - no raw dict/list/tuple.
8
+ """
9
+
10
+ from collections.abc import AsyncIterable, Awaitable, Callable
11
+ from enum import StrEnum
12
+ from typing import TYPE_CHECKING, Final
13
+
14
+ if TYPE_CHECKING:
15
+ from shotgun.agents.models import AgentDeps
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+ # Import SubAgentContext from the main models module
20
+ from shotgun.agents.models import SubAgentContext
21
+
22
+ # Re-export SubAgentContext for convenience
23
+ __all__ = ["SubAgentContext"]
24
+
25
+
26
+ # =============================================================================
27
+ # Mode & Status Enums
28
+ # =============================================================================
29
+
30
+
31
+ class RouterMode(StrEnum):
32
+ """Router execution modes."""
33
+
34
+ PLANNING = "planning" # Incremental, confirmatory - asks before acting
35
+ DRAFTING = "drafting" # Auto-execute - runs full plan without stopping
36
+
37
+
38
+ class PlanApprovalStatus(StrEnum):
39
+ """Status of plan approval in Planning mode."""
40
+
41
+ PENDING = "pending" # Plan shown, awaiting user decision
42
+ APPROVED = "approved" # User approved, ready to execute
43
+ REJECTED = "rejected" # User wants to clarify/modify
44
+ SKIPPED = "skipped" # Simple request, no approval needed
45
+
46
+
47
+ class StepCheckpointAction(StrEnum):
48
+ """User action at step checkpoint (Planning mode only)."""
49
+
50
+ CONTINUE = "continue" # Proceed to next step
51
+ MODIFY = "modify" # User wants to adjust the plan
52
+ STOP = "stop" # Stop execution, keep remaining steps
53
+
54
+
55
+ class CascadeScope(StrEnum):
56
+ """Scope for cascade updates to dependent files."""
57
+
58
+ ALL = "all" # Update all dependent files
59
+ PLAN_ONLY = "plan_only" # Update only plan.md
60
+ TASKS_ONLY = "tasks_only" # Update only tasks.md
61
+ NONE = "none" # Don't update any dependents
62
+
63
+
64
+ class SubAgentResultStatus(StrEnum):
65
+ """Status of sub-agent execution."""
66
+
67
+ SUCCESS = "success"
68
+ PARTIAL = "partial" # Interrupted or incomplete
69
+ ERROR = "error"
70
+ NEEDS_CLARIFICATION = "needs_clarification"
71
+
72
+
73
+ # =============================================================================
74
+ # Execution Plan Models (In-Memory, Not File-Based)
75
+ # =============================================================================
76
+
77
+
78
+ class ExecutionStep(BaseModel):
79
+ """A single step in an execution plan."""
80
+
81
+ id: str = Field(
82
+ ..., description="Human-readable identifier (e.g., 'research-oauth')"
83
+ )
84
+ title: str = Field(..., description="Short title SHOWN to user in plan display")
85
+ objective: str = Field(
86
+ ..., description="Detailed goal HIDDEN from user (for sub-agent)"
87
+ )
88
+ done: bool = Field(
89
+ default=False, description="Whether this step has been completed"
90
+ )
91
+
92
+
93
+ class ExecutionPlan(BaseModel):
94
+ """
95
+ Router's execution plan.
96
+
97
+ Stored IN-MEMORY in RouterDeps, NOT in a file.
98
+ Shown to router in system status message every turn.
99
+ """
100
+
101
+ goal: str = Field(..., description="High-level goal from user request")
102
+ steps: list[ExecutionStep] = Field(
103
+ default_factory=list, description="Ordered list of execution steps"
104
+ )
105
+ current_step_index: int = Field(
106
+ default=0, description="Index of currently executing step"
107
+ )
108
+
109
+ def needs_approval(self) -> bool:
110
+ """
111
+ Determine if plan requires user approval in Planning mode.
112
+
113
+ All plans require approval - user should always see and approve
114
+ the plan before execution begins.
115
+ """
116
+ return len(self.steps) >= 1
117
+
118
+ def current_step(self) -> ExecutionStep | None:
119
+ """Get the current step being executed."""
120
+ if 0 <= self.current_step_index < len(self.steps):
121
+ return self.steps[self.current_step_index]
122
+ return None
123
+
124
+ def next_step(self) -> ExecutionStep | None:
125
+ """Get the next step to execute."""
126
+ next_idx = self.current_step_index + 1
127
+ if next_idx < len(self.steps):
128
+ return self.steps[next_idx]
129
+ return None
130
+
131
+ def is_complete(self) -> bool:
132
+ """Check if all steps are done."""
133
+ return all(step.done for step in self.steps)
134
+
135
+ def pending_steps(self) -> list[ExecutionStep]:
136
+ """Get steps that haven't been completed."""
137
+ return [step for step in self.steps if not step.done]
138
+
139
+ def format_for_display(self) -> str:
140
+ """Format plan for display in system status message."""
141
+ lines = [f"**Goal:** {self.goal}", "", "**Steps:**"]
142
+ for i, step in enumerate(self.steps):
143
+ marker = "✅" if step.done else "⬜"
144
+ current = " ◀" if i == self.current_step_index and not step.done else ""
145
+ lines.append(f"{i + 1}. {marker} {step.title}{current}")
146
+ return "\n".join(lines)
147
+
148
+
149
+ # =============================================================================
150
+ # Tool Input Models (Pydantic only - no dict/list/tuple)
151
+ # =============================================================================
152
+
153
+
154
+ class ExecutionStepInput(BaseModel):
155
+ """Input model for creating a step."""
156
+
157
+ id: str = Field(..., description="Human-readable identifier")
158
+ title: str = Field(..., description="Short title shown to user")
159
+ objective: str = Field(..., description="Detailed goal for sub-agent")
160
+
161
+
162
+ class CreatePlanInput(BaseModel):
163
+ """Input model for create_plan tool."""
164
+
165
+ goal: str = Field(..., description="High-level goal from user request")
166
+ steps: list[ExecutionStepInput] = Field(..., description="Ordered list of steps")
167
+
168
+
169
+ class MarkStepDoneInput(BaseModel):
170
+ """Input model for mark_step_done tool."""
171
+
172
+ step_id: str = Field(..., description="ID of the step to mark as done")
173
+
174
+
175
+ class AddStepInput(BaseModel):
176
+ """Input model for add_step tool."""
177
+
178
+ step: ExecutionStepInput = Field(..., description="The step to add")
179
+ after_step_id: str | None = Field(
180
+ default=None, description="Insert after this step ID (None = append to end)"
181
+ )
182
+
183
+
184
+ class RemoveStepInput(BaseModel):
185
+ """Input model for remove_step tool."""
186
+
187
+ step_id: str = Field(..., description="ID of the step to remove")
188
+
189
+
190
+ class DelegationInput(BaseModel):
191
+ """Input model for delegation tools."""
192
+
193
+ task: str = Field(..., description="The task to delegate to the sub-agent")
194
+ context_hint: str | None = Field(
195
+ default=None, description="Optional context to help the sub-agent"
196
+ )
197
+
198
+
199
+ # =============================================================================
200
+ # Tool Output Models (Pydantic only - no dict/list/tuple)
201
+ # =============================================================================
202
+
203
+
204
+ class ToolResult(BaseModel):
205
+ """Generic result from a tool operation."""
206
+
207
+ success: bool = Field(..., description="Whether the operation succeeded")
208
+ message: str = Field(..., description="Human-readable result message")
209
+
210
+
211
+ class DelegationResult(BaseModel):
212
+ """Result from a sub-agent delegation."""
213
+
214
+ success: bool = Field(..., description="Whether delegation succeeded")
215
+ response: str = Field(default="", description="Sub-agent's response text")
216
+ files_modified: list[str] = Field(
217
+ default_factory=list, description="Files modified by sub-agent"
218
+ )
219
+ has_questions: bool = Field(
220
+ default=False, description="Whether sub-agent has clarifying questions"
221
+ )
222
+ questions: list[str] = Field(
223
+ default_factory=list, description="Clarifying questions from sub-agent"
224
+ )
225
+ error: str | None = Field(default=None, description="Error message if failed")
226
+
227
+
228
+ class SubAgentResult(BaseModel):
229
+ """Full result from a sub-agent execution."""
230
+
231
+ status: SubAgentResultStatus = Field(..., description="Execution status")
232
+ response: str = Field(default="", description="Sub-agent's response text")
233
+ questions: list[str] = Field(
234
+ default_factory=list, description="Clarifying questions from sub-agent (if any)"
235
+ )
236
+ partial_response: str = Field(default="", description="Partial work if interrupted")
237
+ error: str | None = Field(
238
+ default=None, description="Error message if status is ERROR"
239
+ )
240
+ is_retryable: bool = Field(
241
+ default=False, description="Whether the error is transient and retryable"
242
+ )
243
+ files_modified: list[str] = Field(
244
+ default_factory=list, description="Files modified by this sub-agent"
245
+ )
246
+
247
+
248
+ # =============================================================================
249
+ # Pending State Models (for UI coordination)
250
+ # =============================================================================
251
+
252
+
253
+ class PendingCheckpoint(BaseModel):
254
+ """Pending checkpoint state for Planning mode step-by-step execution.
255
+
256
+ Set by mark_step_done tool to trigger checkpoint UI.
257
+ """
258
+
259
+ completed_step: ExecutionStep = Field(
260
+ ..., description="The step that was just completed"
261
+ )
262
+ next_step: ExecutionStep | None = Field(
263
+ default=None, description="The next step to execute, or None if plan complete"
264
+ )
265
+
266
+
267
+ class PendingCascade(BaseModel):
268
+ """Pending cascade confirmation state for Planning mode.
269
+
270
+ Set when a file with dependents is modified and cascade confirmation is needed.
271
+ """
272
+
273
+ updated_file: str = Field(..., description="The file that was just updated")
274
+ dependent_files: list[str] = Field(
275
+ default_factory=list, description="Files that depend on the updated file"
276
+ )
277
+
278
+
279
+ class PendingApproval(BaseModel):
280
+ """Pending approval state for Planning mode multi-step plans.
281
+
282
+ Set by create_plan tool when plan.needs_approval() returns True
283
+ (i.e., the plan has more than one step) in Planning mode.
284
+ """
285
+
286
+ plan: "ExecutionPlan" = Field(..., description="The plan that needs user approval")
287
+
288
+
289
+ # =============================================================================
290
+ # File Dependency Map (for Cascade Confirmation)
291
+ # =============================================================================
292
+
293
+ FILE_DEPENDENCIES: Final[dict[str, tuple[str, ...]]] = {
294
+ "research.md": ("specification.md", "plan.md", "tasks.md"),
295
+ "specification.md": ("plan.md", "tasks.md"),
296
+ "plan.md": ("tasks.md",),
297
+ "tasks.md": (), # Leaf node, no dependents
298
+ }
299
+
300
+
301
+ def get_dependent_files(file_path: str) -> list[str]:
302
+ """Get files that depend on the given file."""
303
+ # Normalize path to just filename
304
+ file_name = file_path.split("/")[-1]
305
+ return list(FILE_DEPENDENCIES.get(file_name, ()))
306
+
307
+
308
+ # =============================================================================
309
+ # RouterDeps (extends AgentDeps)
310
+ # =============================================================================
311
+
312
+ # Import AgentDeps for inheritance - must be done here to avoid circular imports
313
+ from shotgun.agents.models import AgentDeps, AgentType # noqa: E402
314
+
315
+ # Type alias for sub-agent cache entries
316
+ # Each entry is a tuple of (Agent instance, AgentDeps instance)
317
+ # Using object for agent to avoid forward reference issues with pydantic
318
+ SubAgentCacheEntry = tuple[object, AgentDeps]
319
+
320
+ # Type alias for event stream handler callback
321
+ # Matches the signature expected by pydantic_ai's agent.run()
322
+ # Using object to avoid forward reference issues with pydantic
323
+ EventStreamHandler = Callable[[object, AsyncIterable[object]], Awaitable[None]]
324
+
325
+
326
+ class RouterDeps(AgentDeps):
327
+ """
328
+ Router-specific dependencies that extend AgentDeps.
329
+
330
+ This class contains router-specific state on top of the base AgentDeps.
331
+ It is used by the router agent and its tools to manage execution plans
332
+ and sub-agent orchestration.
333
+
334
+ Fields:
335
+ router_mode: Current execution mode (Planning or Drafting)
336
+ current_plan: The execution plan stored in-memory (NOT file-based)
337
+ approval_status: Current approval state for the plan
338
+ active_sub_agent: Which sub-agent is currently executing (for UI)
339
+ is_executing: Whether a plan is currently being executed
340
+ sub_agent_cache: Cached sub-agent instances for lazy initialization
341
+ """
342
+
343
+ router_mode: RouterMode = Field(default=RouterMode.PLANNING)
344
+ current_plan: ExecutionPlan | None = Field(default=None)
345
+ approval_status: PlanApprovalStatus = Field(default=PlanApprovalStatus.SKIPPED)
346
+ active_sub_agent: AgentType | None = Field(default=None)
347
+ is_executing: bool = Field(default=False)
348
+ sub_agent_cache: dict[AgentType, SubAgentCacheEntry] = Field(default_factory=dict)
349
+ # Checkpoint state for Planning mode step-by-step execution
350
+ # Set by mark_step_done tool to trigger checkpoint UI
351
+ # Excluded from serialization as it's transient UI state
352
+ pending_checkpoint: PendingCheckpoint | None = Field(default=None, exclude=True)
353
+ # Cascade confirmation state for Planning mode
354
+ # Set when a file with dependents is modified
355
+ # Excluded from serialization as it's transient UI state
356
+ pending_cascade: PendingCascade | None = Field(default=None, exclude=True)
357
+ # Approval state for Planning mode multi-step plans
358
+ # Set by create_plan tool when plan.needs_approval() returns True
359
+ # Excluded from serialization as it's transient UI state
360
+ pending_approval: PendingApproval | None = Field(default=None, exclude=True)
361
+ # Event stream handler for forwarding sub-agent streaming events to UI
362
+ # This is set by the AgentManager when running the router with streaming
363
+ # Excluded from serialization as it's a callable
364
+ parent_stream_handler: EventStreamHandler | None = Field(
365
+ default=None,
366
+ exclude=True,
367
+ description="Event stream handler from parent context for forwarding sub-agent events",
368
+ )
369
+ # Callback for notifying TUI when plan changes (Stage 11)
370
+ # Set by ChatScreen to receive plan updates for the Plan Panel widget
371
+ # Excluded from serialization as it's a callable
372
+ on_plan_changed: Callable[["ExecutionPlan | None"], None] | None = Field(
373
+ default=None,
374
+ exclude=True,
375
+ description="Callback to notify TUI when plan changes",
376
+ )