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.
- shotgun/agents/agent_manager.py +497 -30
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +90 -77
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +52 -8
- shotgun/agents/config/models.py +48 -45
- shotgun/agents/config/provider.py +44 -29
- shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/export.py +12 -13
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +90 -2
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +384 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +557 -0
- shotgun/agents/router/tools/plan_tools.py +403 -0
- shotgun/agents/runner.py +17 -2
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +81 -3
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +41 -0
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/cli/spec/commands.py +2 -0
- shotgun/cli/spec/models.py +18 -0
- shotgun/cli/spec/pull_service.py +122 -68
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +1 -1
- shotgun/main.py +2 -10
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
- shotgun/prompts/agents/plan.j2 +43 -1
- shotgun/prompts/agents/research.j2 +75 -20
- shotgun/prompts/agents/router.j2 +713 -0
- shotgun/prompts/agents/specify.j2 +94 -4
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +24 -15
- shotgun/prompts/agents/tasks.j2 +77 -23
- shotgun/settings.py +44 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
- shotgun/tui/app.py +90 -23
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/prompt_input.py +23 -28
- shotgun/tui/components/status_bar.py +5 -4
- shotgun/tui/dependencies.py +58 -8
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +24 -1
- shotgun/tui/screens/chat/chat_screen.py +1374 -211
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -97
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
- shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +14 -9
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/screens/shotgun_auth.py +50 -0
- shotgun/tui/screens/spec_pull.py +2 -0
- shotgun/tui/state/processing_state.py +19 -0
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- shotgun/tui/widgets/widget_coordinator.py +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
- shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/tui/screens/onboarding.py +0 -580
- shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
- {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
|
+
]
|