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,384 @@
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
+ files_found: list[str] = Field(
220
+ default_factory=list,
221
+ description="Files found by sub-agent (used by FileReadAgent)",
222
+ )
223
+ has_questions: bool = Field(
224
+ default=False, description="Whether sub-agent has clarifying questions"
225
+ )
226
+ questions: list[str] = Field(
227
+ default_factory=list, description="Clarifying questions from sub-agent"
228
+ )
229
+ error: str | None = Field(default=None, description="Error message if failed")
230
+
231
+
232
+ class SubAgentResult(BaseModel):
233
+ """Full result from a sub-agent execution."""
234
+
235
+ status: SubAgentResultStatus = Field(..., description="Execution status")
236
+ response: str = Field(default="", description="Sub-agent's response text")
237
+ questions: list[str] = Field(
238
+ default_factory=list, description="Clarifying questions from sub-agent (if any)"
239
+ )
240
+ partial_response: str = Field(default="", description="Partial work if interrupted")
241
+ error: str | None = Field(
242
+ default=None, description="Error message if status is ERROR"
243
+ )
244
+ is_retryable: bool = Field(
245
+ default=False, description="Whether the error is transient and retryable"
246
+ )
247
+ files_modified: list[str] = Field(
248
+ default_factory=list, description="Files modified by this sub-agent"
249
+ )
250
+
251
+
252
+ # =============================================================================
253
+ # Pending State Models (for UI coordination)
254
+ # =============================================================================
255
+
256
+
257
+ class PendingCheckpoint(BaseModel):
258
+ """Pending checkpoint state for Planning mode step-by-step execution.
259
+
260
+ Set by mark_step_done tool to trigger checkpoint UI.
261
+ """
262
+
263
+ completed_step: ExecutionStep = Field(
264
+ ..., description="The step that was just completed"
265
+ )
266
+ next_step: ExecutionStep | None = Field(
267
+ default=None, description="The next step to execute, or None if plan complete"
268
+ )
269
+
270
+
271
+ class PendingCascade(BaseModel):
272
+ """Pending cascade confirmation state for Planning mode.
273
+
274
+ Set when a file with dependents is modified and cascade confirmation is needed.
275
+ """
276
+
277
+ updated_file: str = Field(..., description="The file that was just updated")
278
+ dependent_files: list[str] = Field(
279
+ default_factory=list, description="Files that depend on the updated file"
280
+ )
281
+
282
+
283
+ class PendingApproval(BaseModel):
284
+ """Pending approval state for Planning mode multi-step plans.
285
+
286
+ Set by create_plan tool when plan.needs_approval() returns True
287
+ (i.e., the plan has more than one step) in Planning mode.
288
+ """
289
+
290
+ plan: "ExecutionPlan" = Field(..., description="The plan that needs user approval")
291
+
292
+
293
+ # =============================================================================
294
+ # File Dependency Map (for Cascade Confirmation)
295
+ # =============================================================================
296
+
297
+ FILE_DEPENDENCIES: Final[dict[str, tuple[str, ...]]] = {
298
+ "research.md": ("specification.md", "plan.md", "tasks.md"),
299
+ "specification.md": ("plan.md", "tasks.md"),
300
+ "plan.md": ("tasks.md",),
301
+ "tasks.md": (), # Leaf node, no dependents
302
+ }
303
+
304
+
305
+ def get_dependent_files(file_path: str) -> list[str]:
306
+ """Get files that depend on the given file."""
307
+ # Normalize path to just filename
308
+ file_name = file_path.split("/")[-1]
309
+ return list(FILE_DEPENDENCIES.get(file_name, ()))
310
+
311
+
312
+ # =============================================================================
313
+ # RouterDeps (extends AgentDeps)
314
+ # =============================================================================
315
+
316
+ # Import AgentDeps for inheritance - must be done here to avoid circular imports
317
+ from shotgun.agents.models import AgentDeps, AgentType # noqa: E402
318
+
319
+ # Type alias for sub-agent cache entries
320
+ # Each entry is a tuple of (Agent instance, AgentDeps instance)
321
+ # Using object for agent to avoid forward reference issues with pydantic
322
+ SubAgentCacheEntry = tuple[object, AgentDeps]
323
+
324
+ # Type alias for event stream handler callback
325
+ # Matches the signature expected by pydantic_ai's agent.run()
326
+ # Using object to avoid forward reference issues with pydantic
327
+ EventStreamHandler = Callable[[object, AsyncIterable[object]], Awaitable[None]]
328
+
329
+
330
+ class RouterDeps(AgentDeps):
331
+ """
332
+ Router-specific dependencies that extend AgentDeps.
333
+
334
+ This class contains router-specific state on top of the base AgentDeps.
335
+ It is used by the router agent and its tools to manage execution plans
336
+ and sub-agent orchestration.
337
+
338
+ Fields:
339
+ router_mode: Current execution mode (Planning or Drafting)
340
+ current_plan: The execution plan stored in-memory (NOT file-based)
341
+ approval_status: Current approval state for the plan
342
+ active_sub_agent: Which sub-agent is currently executing (for UI)
343
+ is_executing: Whether a plan is currently being executed
344
+ sub_agent_cache: Cached sub-agent instances for lazy initialization
345
+ """
346
+
347
+ router_mode: RouterMode = Field(default=RouterMode.PLANNING)
348
+ current_plan: ExecutionPlan | None = Field(default=None)
349
+ approval_status: PlanApprovalStatus = Field(default=PlanApprovalStatus.SKIPPED)
350
+ active_sub_agent: AgentType | None = Field(default=None)
351
+ is_executing: bool = Field(default=False)
352
+ sub_agent_cache: dict[AgentType, SubAgentCacheEntry] = Field(default_factory=dict)
353
+ # Checkpoint state for Planning mode step-by-step execution
354
+ # Set by mark_step_done tool to trigger checkpoint UI
355
+ # Excluded from serialization as it's transient UI state
356
+ pending_checkpoint: PendingCheckpoint | None = Field(default=None, exclude=True)
357
+ # Cascade confirmation state for Planning mode
358
+ # Set when a file with dependents is modified
359
+ # Excluded from serialization as it's transient UI state
360
+ pending_cascade: PendingCascade | None = Field(default=None, exclude=True)
361
+ # Approval state for Planning mode multi-step plans
362
+ # Set by create_plan tool when plan.needs_approval() returns True
363
+ # Excluded from serialization as it's transient UI state
364
+ pending_approval: PendingApproval | None = Field(default=None, exclude=True)
365
+ # Completion state for Drafting mode
366
+ # Set by mark_step_done when plan completes in drafting mode
367
+ # Excluded from serialization as it's transient UI state
368
+ pending_completion: bool = Field(default=False, exclude=True)
369
+ # Event stream handler for forwarding sub-agent streaming events to UI
370
+ # This is set by the AgentManager when running the router with streaming
371
+ # Excluded from serialization as it's a callable
372
+ parent_stream_handler: EventStreamHandler | None = Field(
373
+ default=None,
374
+ exclude=True,
375
+ description="Event stream handler from parent context for forwarding sub-agent events",
376
+ )
377
+ # Callback for notifying TUI when plan changes (Stage 11)
378
+ # Set by ChatScreen to receive plan updates for the Plan Panel widget
379
+ # Excluded from serialization as it's a callable
380
+ on_plan_changed: Callable[["ExecutionPlan | None"], None] | None = Field(
381
+ default=None,
382
+ exclude=True,
383
+ description="Callback to notify TUI when plan changes",
384
+ )
@@ -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
+ ]