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.
- shotgun/agents/agent_manager.py +219 -37
- shotgun/agents/common.py +79 -78
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +364 -53
- shotgun/agents/config/models.py +101 -21
- shotgun/agents/config/provider.py +51 -13
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/analyzer.py +6 -2
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +27 -1
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +12 -13
- shotgun/agents/models.py +66 -1
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +376 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +503 -0
- shotgun/agents/router/tools/plan_tools.py +322 -0
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/file_management.py +49 -1
- shotgun/agents/tools/registry.py +2 -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 +1 -1
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +44 -1
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/codebase/core/change_detector.py +1 -1
- shotgun/codebase/core/ingestor.py +154 -8
- shotgun/codebase/core/manager.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +325 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +42 -0
- shotgun/main.py +4 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
- shotgun/prompts/agents/plan.j2 +29 -1
- shotgun/prompts/agents/research.j2 +75 -23
- shotgun/prompts/agents/router.j2 +440 -0
- shotgun/prompts/agents/specify.j2 +80 -4
- shotgun/prompts/agents/state/system_state.j2 +15 -8
- shotgun/prompts/agents/tasks.j2 +63 -23
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/settings.py +5 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/tui/app.py +78 -15
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/status_bar.py +2 -2
- shotgun/tui/containers.py +1 -1
- shotgun/tui/dependencies.py +64 -9
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +9 -1
- shotgun/tui/screens/chat/chat_screen.py +1015 -106
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -89
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +28 -8
- shotgun/tui/screens/onboarding.py +179 -26
- shotgun/tui/screens/pipx_migration.py +58 -6
- shotgun/tui/screens/provider_config.py +66 -8
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +110 -16
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +123 -0
- shotgun/tui/services/conversation_service.py +5 -2
- 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 +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
- shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
- shotgun_sh-0.2.17.dist-info/RECORD +0 -194
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Plan management tools for the Router agent.
|
|
2
|
+
|
|
3
|
+
These tools allow the router to create and manage execution plans.
|
|
4
|
+
All tools use Pydantic models for inputs and outputs.
|
|
5
|
+
|
|
6
|
+
IMPORTANT: There is NO get_plan() tool - the plan is shown in the system
|
|
7
|
+
status message every turn so the router always has visibility into it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pydantic_ai import RunContext
|
|
11
|
+
|
|
12
|
+
from shotgun.agents.router.models import (
|
|
13
|
+
AddStepInput,
|
|
14
|
+
CreatePlanInput,
|
|
15
|
+
ExecutionPlan,
|
|
16
|
+
ExecutionStep,
|
|
17
|
+
MarkStepDoneInput,
|
|
18
|
+
PendingApproval,
|
|
19
|
+
PendingCheckpoint,
|
|
20
|
+
PlanApprovalStatus,
|
|
21
|
+
RemoveStepInput,
|
|
22
|
+
RouterDeps,
|
|
23
|
+
RouterMode,
|
|
24
|
+
ToolResult,
|
|
25
|
+
)
|
|
26
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
27
|
+
from shotgun.logging_config import get_logger
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _notify_plan_changed(deps: RouterDeps) -> None:
|
|
33
|
+
"""Notify TUI of plan changes via callback if registered.
|
|
34
|
+
|
|
35
|
+
This helper is called after any plan modification to update the
|
|
36
|
+
Plan Panel widget in the TUI.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
deps: RouterDeps containing the on_plan_changed callback.
|
|
40
|
+
"""
|
|
41
|
+
if deps.on_plan_changed:
|
|
42
|
+
deps.on_plan_changed(deps.current_plan)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@register_tool(
|
|
46
|
+
category=ToolCategory.PLANNING,
|
|
47
|
+
display_text="Creating execution plan",
|
|
48
|
+
key_arg="input",
|
|
49
|
+
)
|
|
50
|
+
async def create_plan(
|
|
51
|
+
ctx: RunContext[RouterDeps], input: CreatePlanInput
|
|
52
|
+
) -> ToolResult:
|
|
53
|
+
"""Create a new execution plan for the current task.
|
|
54
|
+
|
|
55
|
+
This replaces any existing plan. The plan is stored in-memory in RouterDeps,
|
|
56
|
+
NOT in a file. It will be shown in the system status message.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
ctx: RunContext with RouterDeps
|
|
60
|
+
input: CreatePlanInput with goal and steps
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
ToolResult indicating success or failure
|
|
64
|
+
"""
|
|
65
|
+
logger.debug("Creating execution plan with goal: %s", input.goal)
|
|
66
|
+
|
|
67
|
+
# Convert step inputs to ExecutionStep objects
|
|
68
|
+
steps = [
|
|
69
|
+
ExecutionStep(
|
|
70
|
+
id=step_input.id,
|
|
71
|
+
title=step_input.title,
|
|
72
|
+
objective=step_input.objective,
|
|
73
|
+
done=False,
|
|
74
|
+
)
|
|
75
|
+
for step_input in input.steps
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# Create and store the plan
|
|
79
|
+
plan = ExecutionPlan(
|
|
80
|
+
goal=input.goal,
|
|
81
|
+
steps=steps,
|
|
82
|
+
current_step_index=0,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
ctx.deps.current_plan = plan
|
|
86
|
+
|
|
87
|
+
# Set pending approval for multi-step plans in Planning mode
|
|
88
|
+
# The TUI will detect this and show the PlanApprovalWidget
|
|
89
|
+
if ctx.deps.router_mode == RouterMode.PLANNING and plan.needs_approval():
|
|
90
|
+
ctx.deps.pending_approval = PendingApproval(plan=plan)
|
|
91
|
+
ctx.deps.approval_status = PlanApprovalStatus.PENDING
|
|
92
|
+
# Plan is NOT executing yet - user must approve first
|
|
93
|
+
ctx.deps.is_executing = False
|
|
94
|
+
logger.debug(
|
|
95
|
+
"Set pending approval for plan with %d steps",
|
|
96
|
+
len(steps),
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
# Single-step plans or Drafting mode - skip approval and start executing
|
|
100
|
+
ctx.deps.approval_status = PlanApprovalStatus.SKIPPED
|
|
101
|
+
ctx.deps.is_executing = True
|
|
102
|
+
logger.debug("Plan approved automatically, is_executing=True")
|
|
103
|
+
|
|
104
|
+
logger.info(
|
|
105
|
+
"Created execution plan with %d steps: %s",
|
|
106
|
+
len(steps),
|
|
107
|
+
input.goal,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
_notify_plan_changed(ctx.deps)
|
|
111
|
+
|
|
112
|
+
# Return different message based on whether approval is needed
|
|
113
|
+
if ctx.deps.pending_approval is not None:
|
|
114
|
+
return ToolResult(
|
|
115
|
+
success=True,
|
|
116
|
+
message=f"Created plan with {len(steps)} steps. Goal: {input.goal}\n\n"
|
|
117
|
+
"IMPORTANT: This plan requires user approval before execution. "
|
|
118
|
+
"You MUST call final_result NOW to present this plan to the user. "
|
|
119
|
+
"Do NOT attempt to delegate or execute any steps yet.",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return ToolResult(
|
|
123
|
+
success=True,
|
|
124
|
+
message=f"Created plan with {len(steps)} steps. Goal: {input.goal}",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@register_tool(
|
|
129
|
+
category=ToolCategory.PLANNING,
|
|
130
|
+
display_text="Marking step complete",
|
|
131
|
+
key_arg="input",
|
|
132
|
+
)
|
|
133
|
+
async def mark_step_done(
|
|
134
|
+
ctx: RunContext[RouterDeps], input: MarkStepDoneInput
|
|
135
|
+
) -> ToolResult:
|
|
136
|
+
"""Mark a step in the execution plan as complete.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
ctx: RunContext with RouterDeps
|
|
140
|
+
input: MarkStepDoneInput with step_id
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
ToolResult indicating success or failure
|
|
144
|
+
"""
|
|
145
|
+
plan = ctx.deps.current_plan
|
|
146
|
+
|
|
147
|
+
if plan is None:
|
|
148
|
+
return ToolResult(
|
|
149
|
+
success=False,
|
|
150
|
+
message="No execution plan exists. Create a plan first.",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Find the step by ID
|
|
154
|
+
for _i, step in enumerate(plan.steps):
|
|
155
|
+
if step.id == input.step_id:
|
|
156
|
+
step.done = True
|
|
157
|
+
logger.info("Marked step '%s' as done", input.step_id)
|
|
158
|
+
|
|
159
|
+
# Advance current_step_index to next incomplete step
|
|
160
|
+
while (
|
|
161
|
+
plan.current_step_index < len(plan.steps)
|
|
162
|
+
and plan.steps[plan.current_step_index].done
|
|
163
|
+
):
|
|
164
|
+
plan.current_step_index += 1
|
|
165
|
+
|
|
166
|
+
# Check if plan is complete
|
|
167
|
+
if plan.is_complete():
|
|
168
|
+
ctx.deps.is_executing = False
|
|
169
|
+
logger.debug("Plan complete, is_executing=False")
|
|
170
|
+
# Set pending checkpoint for Planning mode
|
|
171
|
+
# The TUI will detect this and show the StepCheckpointWidget
|
|
172
|
+
elif ctx.deps.router_mode == RouterMode.PLANNING:
|
|
173
|
+
# Use current_step() since the while loop above already advanced
|
|
174
|
+
# current_step_index to the next incomplete step
|
|
175
|
+
next_step = plan.current_step()
|
|
176
|
+
ctx.deps.pending_checkpoint = PendingCheckpoint(
|
|
177
|
+
completed_step=step, next_step=next_step
|
|
178
|
+
)
|
|
179
|
+
logger.debug(
|
|
180
|
+
"Set pending checkpoint: completed='%s', next='%s'",
|
|
181
|
+
step.title,
|
|
182
|
+
next_step.title if next_step else None,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
_notify_plan_changed(ctx.deps)
|
|
186
|
+
|
|
187
|
+
return ToolResult(
|
|
188
|
+
success=True,
|
|
189
|
+
message=f"Marked step '{step.title}' as complete.",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return ToolResult(
|
|
193
|
+
success=False,
|
|
194
|
+
message=f"Step with ID '{input.step_id}' not found in plan.",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@register_tool(
|
|
199
|
+
category=ToolCategory.PLANNING,
|
|
200
|
+
display_text="Adding step to plan",
|
|
201
|
+
key_arg="input",
|
|
202
|
+
)
|
|
203
|
+
async def add_step(ctx: RunContext[RouterDeps], input: AddStepInput) -> ToolResult:
|
|
204
|
+
"""Add a new step to the execution plan.
|
|
205
|
+
|
|
206
|
+
The step can be inserted after a specific step (by ID) or appended to the end.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
ctx: RunContext with RouterDeps
|
|
210
|
+
input: AddStepInput with step details and optional after_step_id
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
ToolResult indicating success or failure
|
|
214
|
+
"""
|
|
215
|
+
plan = ctx.deps.current_plan
|
|
216
|
+
|
|
217
|
+
if plan is None:
|
|
218
|
+
return ToolResult(
|
|
219
|
+
success=False,
|
|
220
|
+
message="No execution plan exists. Create a plan first.",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Check for duplicate ID
|
|
224
|
+
existing_ids = {step.id for step in plan.steps}
|
|
225
|
+
if input.step.id in existing_ids:
|
|
226
|
+
return ToolResult(
|
|
227
|
+
success=False,
|
|
228
|
+
message=f"Step with ID '{input.step.id}' already exists in plan.",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Create the new step
|
|
232
|
+
new_step = ExecutionStep(
|
|
233
|
+
id=input.step.id,
|
|
234
|
+
title=input.step.title,
|
|
235
|
+
objective=input.step.objective,
|
|
236
|
+
done=False,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Insert at the specified position
|
|
240
|
+
if input.after_step_id is None:
|
|
241
|
+
# Append to end
|
|
242
|
+
plan.steps.append(new_step)
|
|
243
|
+
logger.info("Appended step '%s' to end of plan", input.step.id)
|
|
244
|
+
|
|
245
|
+
_notify_plan_changed(ctx.deps)
|
|
246
|
+
|
|
247
|
+
return ToolResult(
|
|
248
|
+
success=True,
|
|
249
|
+
message=f"Added step '{new_step.title}' at end of plan.",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Find the position to insert after
|
|
253
|
+
for i, step in enumerate(plan.steps):
|
|
254
|
+
if step.id == input.after_step_id:
|
|
255
|
+
plan.steps.insert(i + 1, new_step)
|
|
256
|
+
logger.info(
|
|
257
|
+
"Inserted step '%s' after '%s'",
|
|
258
|
+
input.step.id,
|
|
259
|
+
input.after_step_id,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
_notify_plan_changed(ctx.deps)
|
|
263
|
+
|
|
264
|
+
return ToolResult(
|
|
265
|
+
success=True,
|
|
266
|
+
message=f"Added step '{new_step.title}' after '{step.title}'.",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return ToolResult(
|
|
270
|
+
success=False,
|
|
271
|
+
message=f"Step with ID '{input.after_step_id}' not found in plan.",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@register_tool(
|
|
276
|
+
category=ToolCategory.PLANNING,
|
|
277
|
+
display_text="Removing step from plan",
|
|
278
|
+
key_arg="input",
|
|
279
|
+
)
|
|
280
|
+
async def remove_step(
|
|
281
|
+
ctx: RunContext[RouterDeps], input: RemoveStepInput
|
|
282
|
+
) -> ToolResult:
|
|
283
|
+
"""Remove a step from the execution plan.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
ctx: RunContext with RouterDeps
|
|
287
|
+
input: RemoveStepInput with step_id
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
ToolResult indicating success or failure
|
|
291
|
+
"""
|
|
292
|
+
plan = ctx.deps.current_plan
|
|
293
|
+
|
|
294
|
+
if plan is None:
|
|
295
|
+
return ToolResult(
|
|
296
|
+
success=False,
|
|
297
|
+
message="No execution plan exists. Create a plan first.",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Find and remove the step
|
|
301
|
+
for i, step in enumerate(plan.steps):
|
|
302
|
+
if step.id == input.step_id:
|
|
303
|
+
removed_step = plan.steps.pop(i)
|
|
304
|
+
logger.info("Removed step '%s' from plan", input.step_id)
|
|
305
|
+
|
|
306
|
+
# Adjust current_step_index if needed
|
|
307
|
+
if plan.current_step_index > i:
|
|
308
|
+
plan.current_step_index -= 1
|
|
309
|
+
elif plan.current_step_index >= len(plan.steps):
|
|
310
|
+
plan.current_step_index = max(0, len(plan.steps) - 1)
|
|
311
|
+
|
|
312
|
+
_notify_plan_changed(ctx.deps)
|
|
313
|
+
|
|
314
|
+
return ToolResult(
|
|
315
|
+
success=True,
|
|
316
|
+
message=f"Removed step '{removed_step.title}' from plan.",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return ToolResult(
|
|
320
|
+
success=False,
|
|
321
|
+
message=f"Step with ID '{input.step_id}' not found in plan.",
|
|
322
|
+
)
|
shotgun/agents/runner.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Unified agent execution with consistent error handling.
|
|
2
|
+
|
|
3
|
+
This module provides a reusable agent runner that wraps agent execution exceptions
|
|
4
|
+
in user-friendly custom exceptions that can be caught and displayed by TUI or CLI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
10
|
+
|
|
11
|
+
from anthropic import APIStatusError as AnthropicAPIStatusError
|
|
12
|
+
from openai import APIStatusError as OpenAIAPIStatusError
|
|
13
|
+
from pydantic_ai.exceptions import ModelHTTPError, UnexpectedModelBehavior
|
|
14
|
+
|
|
15
|
+
from shotgun.agents.error.models import AgentErrorContext
|
|
16
|
+
from shotgun.exceptions import (
|
|
17
|
+
AgentCancelledException,
|
|
18
|
+
BudgetExceededException,
|
|
19
|
+
BYOKAuthenticationException,
|
|
20
|
+
BYOKGenericAPIException,
|
|
21
|
+
BYOKQuotaBillingException,
|
|
22
|
+
BYOKRateLimitException,
|
|
23
|
+
BYOKServiceOverloadException,
|
|
24
|
+
ContextSizeLimitExceeded,
|
|
25
|
+
GenericAPIStatusException,
|
|
26
|
+
ShotgunRateLimitException,
|
|
27
|
+
ShotgunServiceOverloadException,
|
|
28
|
+
UnknownAgentException,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from shotgun.agents.agent_manager import AgentManager
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentRunner:
|
|
38
|
+
"""Unified agent execution wrapper with consistent error handling.
|
|
39
|
+
|
|
40
|
+
This class wraps agent execution and converts any exceptions into
|
|
41
|
+
user-friendly custom exceptions that can be caught and displayed by the
|
|
42
|
+
calling interface (TUI or CLI).
|
|
43
|
+
|
|
44
|
+
The runner:
|
|
45
|
+
- Executes the agent
|
|
46
|
+
- Logs errors for debugging
|
|
47
|
+
- Wraps exceptions in custom exception types (AgentCancelledException,
|
|
48
|
+
BYOKRateLimitException, etc.)
|
|
49
|
+
- Lets exceptions propagate to caller for display
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> runner = AgentRunner(agent_manager)
|
|
53
|
+
>>> try:
|
|
54
|
+
>>> await runner.run("Write a hello world function")
|
|
55
|
+
>>> except ContextSizeLimitExceeded as e:
|
|
56
|
+
>>> print(e.to_markdown())
|
|
57
|
+
>>> except BYOKRateLimitException as e:
|
|
58
|
+
>>> print(e.to_plain_text())
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, agent_manager: "AgentManager"):
|
|
62
|
+
"""Initialize the agent runner.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
agent_manager: The agent manager to execute
|
|
66
|
+
"""
|
|
67
|
+
self.agent_manager = agent_manager
|
|
68
|
+
|
|
69
|
+
async def run(self, prompt: str) -> None:
|
|
70
|
+
"""Run the agent with the given prompt.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
prompt: The user's prompt/query
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
Custom exceptions for different error types:
|
|
77
|
+
- AgentCancelledException: User cancelled the operation
|
|
78
|
+
- ContextSizeLimitExceeded: Context too large for model
|
|
79
|
+
- BudgetExceededException: Shotgun Account budget exceeded
|
|
80
|
+
- BYOKRateLimitException: BYOK rate limit hit
|
|
81
|
+
- BYOKQuotaBillingException: BYOK quota/billing issue
|
|
82
|
+
- BYOKAuthenticationException: BYOK authentication failed
|
|
83
|
+
- BYOKServiceOverloadException: BYOK service overloaded
|
|
84
|
+
- BYOKGenericAPIException: Generic BYOK API error
|
|
85
|
+
- ShotgunServiceOverloadException: Shotgun service overloaded
|
|
86
|
+
- ShotgunRateLimitException: Shotgun rate limit hit
|
|
87
|
+
- GenericAPIStatusException: Generic API error
|
|
88
|
+
- UnknownAgentException: Unknown/unclassified error
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
await self.agent_manager.run(prompt=prompt)
|
|
92
|
+
|
|
93
|
+
except asyncio.CancelledError as e:
|
|
94
|
+
# User cancelled - wrap and re-raise as our custom exception
|
|
95
|
+
context = self._create_error_context(e)
|
|
96
|
+
self._classify_and_raise(context)
|
|
97
|
+
|
|
98
|
+
except ContextSizeLimitExceeded as e:
|
|
99
|
+
# Already a custom exception - log and re-raise
|
|
100
|
+
logger.info(
|
|
101
|
+
"Context size limit exceeded",
|
|
102
|
+
extra={
|
|
103
|
+
"max_tokens": e.max_tokens,
|
|
104
|
+
"model_name": e.model_name,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
# Log with full stack trace to shotgun.log
|
|
111
|
+
logger.exception(
|
|
112
|
+
"Agent run failed",
|
|
113
|
+
extra={
|
|
114
|
+
"agent_mode": self.agent_manager._current_agent_type.value,
|
|
115
|
+
"error_type": type(e).__name__,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Create error context and wrap/raise custom exception
|
|
120
|
+
context = self._create_error_context(e)
|
|
121
|
+
self._classify_and_raise(context)
|
|
122
|
+
|
|
123
|
+
def _create_error_context(self, exception: BaseException) -> AgentErrorContext:
|
|
124
|
+
"""Create error context from exception and agent state.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
exception: The exception that was raised
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
AgentErrorContext with all necessary information for classification
|
|
131
|
+
"""
|
|
132
|
+
return AgentErrorContext(
|
|
133
|
+
exception=exception,
|
|
134
|
+
is_shotgun_account=self.agent_manager.deps.llm_model.is_shotgun_account,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _classify_and_raise(self, context: AgentErrorContext) -> NoReturn:
|
|
138
|
+
"""Classify an exception and raise the appropriate custom exception.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
context: Context information about the error
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
Custom exception based on the error type
|
|
145
|
+
"""
|
|
146
|
+
exception = context.exception
|
|
147
|
+
error_name = type(exception).__name__
|
|
148
|
+
error_message = str(exception)
|
|
149
|
+
|
|
150
|
+
# Check for cancellation
|
|
151
|
+
if isinstance(exception, asyncio.CancelledError):
|
|
152
|
+
raise AgentCancelledException() from exception
|
|
153
|
+
|
|
154
|
+
# Check for context size limit exceeded
|
|
155
|
+
if isinstance(exception, ContextSizeLimitExceeded):
|
|
156
|
+
# Already the right exception type, re-raise it
|
|
157
|
+
raise exception
|
|
158
|
+
|
|
159
|
+
# Check for budget exceeded (Shotgun Account only)
|
|
160
|
+
if (
|
|
161
|
+
context.is_shotgun_account
|
|
162
|
+
and "apistatuserror" in error_name.lower()
|
|
163
|
+
and "budget" in error_message.lower()
|
|
164
|
+
and "exceeded" in error_message.lower()
|
|
165
|
+
):
|
|
166
|
+
raise BudgetExceededException(message=error_message) from exception
|
|
167
|
+
|
|
168
|
+
# Check for empty model response (e.g., model unavailable or misconfigured)
|
|
169
|
+
if isinstance(exception, UnexpectedModelBehavior):
|
|
170
|
+
raise GenericAPIStatusException(
|
|
171
|
+
"The model returned an empty response. This may indicate:\n"
|
|
172
|
+
"- The model is unavailable or misconfigured\n"
|
|
173
|
+
"- A temporary service issue\n\n"
|
|
174
|
+
"Try switching to a different model or try again later."
|
|
175
|
+
) from exception
|
|
176
|
+
|
|
177
|
+
# Detect API errors
|
|
178
|
+
is_api_error = False
|
|
179
|
+
if isinstance(exception, OpenAIAPIStatusError):
|
|
180
|
+
is_api_error = True
|
|
181
|
+
elif isinstance(exception, AnthropicAPIStatusError):
|
|
182
|
+
is_api_error = True
|
|
183
|
+
elif isinstance(exception, ModelHTTPError):
|
|
184
|
+
# pydantic_ai wraps API errors in ModelHTTPError
|
|
185
|
+
# Check for HTTP error status codes (4xx client errors)
|
|
186
|
+
if 400 <= exception.status_code < 500:
|
|
187
|
+
is_api_error = True
|
|
188
|
+
|
|
189
|
+
# BYOK user API errors
|
|
190
|
+
if not context.is_shotgun_account and is_api_error:
|
|
191
|
+
self._raise_byok_api_error(error_message, exception)
|
|
192
|
+
|
|
193
|
+
# Shotgun Account specific errors
|
|
194
|
+
if "APIStatusError" in error_name:
|
|
195
|
+
if "overload" in error_message.lower():
|
|
196
|
+
raise ShotgunServiceOverloadException(error_message) from exception
|
|
197
|
+
elif "rate" in error_message.lower():
|
|
198
|
+
raise ShotgunRateLimitException(error_message) from exception
|
|
199
|
+
else:
|
|
200
|
+
raise GenericAPIStatusException(error_message) from exception
|
|
201
|
+
|
|
202
|
+
# Unknown error - wrap in our custom exception
|
|
203
|
+
raise UnknownAgentException(exception) from exception
|
|
204
|
+
|
|
205
|
+
def _raise_byok_api_error(
|
|
206
|
+
self, error_message: str, original_exception: Exception
|
|
207
|
+
) -> NoReturn:
|
|
208
|
+
"""Classify and raise API errors for BYOK users into specific types.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
error_message: The error message from the API
|
|
212
|
+
original_exception: The original exception
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
Specific BYOK exception type
|
|
216
|
+
"""
|
|
217
|
+
error_lower = error_message.lower()
|
|
218
|
+
|
|
219
|
+
if "rate" in error_lower:
|
|
220
|
+
raise BYOKRateLimitException(error_message) from original_exception
|
|
221
|
+
elif "quota" in error_lower or "billing" in error_lower:
|
|
222
|
+
raise BYOKQuotaBillingException(error_message) from original_exception
|
|
223
|
+
elif "authentication" in error_lower or (
|
|
224
|
+
"invalid" in error_lower and "key" in error_lower
|
|
225
|
+
):
|
|
226
|
+
raise BYOKAuthenticationException(error_message) from original_exception
|
|
227
|
+
elif "overload" in error_lower:
|
|
228
|
+
raise BYOKServiceOverloadException(error_message) from original_exception
|
|
229
|
+
else:
|
|
230
|
+
raise BYOKGenericAPIException(error_message) from original_exception
|
shotgun/agents/specify.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_specify_agent(
|
|
27
26
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
28
|
-
) -> tuple[
|
|
27
|
+
) -> tuple[ShotgunAgent, AgentDeps]:
|
|
29
28
|
"""Create a specify agent with artifact management capabilities.
|
|
30
29
|
|
|
31
30
|
Args:
|
|
@@ -51,26 +50,25 @@ async def create_specify_agent(
|
|
|
51
50
|
|
|
52
51
|
|
|
53
52
|
async def run_specify_agent(
|
|
54
|
-
agent:
|
|
55
|
-
|
|
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 specifications based on the given
|
|
59
|
+
"""Create or update specifications based on the given prompt.
|
|
60
60
|
|
|
61
61
|
Args:
|
|
62
62
|
agent: The configured specify agent
|
|
63
|
-
|
|
63
|
+
prompt: The specification 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 specification process output
|
|
69
70
|
"""
|
|
70
|
-
logger.debug("📋 Starting specification for
|
|
71
|
-
|
|
72
|
-
# Simple prompt - the agent system prompt has all the artifact instructions
|
|
73
|
-
full_prompt = f"Create a comprehensive specification for: {requirement}"
|
|
71
|
+
logger.debug("📋 Starting specification 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_specify_agent(
|
|
|
80
78
|
|
|
81
79
|
result = await run_agent(
|
|
82
80
|
agent=agent,
|
|
83
|
-
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("✅ Specification completed successfully")
|
shotgun/agents/tasks.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_tasks_agent(
|
|
27
26
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
28
|
-
) -> tuple[
|
|
27
|
+
) -> tuple[ShotgunAgent, AgentDeps]:
|
|
29
28
|
"""Create a tasks agent with file management capabilities.
|
|
30
29
|
|
|
31
30
|
Args:
|
|
@@ -49,39 +48,39 @@ async def create_tasks_agent(
|
|
|
49
48
|
|
|
50
49
|
|
|
51
50
|
async def run_tasks_agent(
|
|
52
|
-
agent:
|
|
53
|
-
|
|
51
|
+
agent: ShotgunAgent,
|
|
52
|
+
prompt: str,
|
|
54
53
|
deps: AgentDeps,
|
|
55
54
|
message_history: list[ModelMessage] | None = None,
|
|
55
|
+
event_stream_handler: EventStreamHandler | None = None,
|
|
56
56
|
) -> AgentRunResult[AgentResponse]:
|
|
57
|
-
"""Create or update tasks based on the given
|
|
57
|
+
"""Create or update tasks based on the given prompt.
|
|
58
58
|
|
|
59
59
|
Args:
|
|
60
60
|
agent: The configured tasks agent
|
|
61
|
-
|
|
61
|
+
prompt: The task creation/update prompt
|
|
62
62
|
deps: Agent dependencies
|
|
63
63
|
message_history: Optional message history for conversation continuity
|
|
64
|
+
event_stream_handler: Optional callback for streaming events
|
|
64
65
|
|
|
65
66
|
Returns:
|
|
66
67
|
AgentRunResult containing the task creation process output
|
|
67
68
|
"""
|
|
68
|
-
logger.debug("📋 Starting task creation for
|
|
69
|
+
logger.debug("📋 Starting task creation for prompt: %s", prompt)
|
|
69
70
|
|
|
70
71
|
message_history = await add_system_status_message(deps, message_history)
|
|
71
72
|
|
|
72
|
-
# Let the agent use its tools to read existing tasks, plan, and research
|
|
73
|
-
full_prompt = f"Create or update tasks based on: {instruction}"
|
|
74
|
-
|
|
75
73
|
try:
|
|
76
74
|
# Create usage limits for responsible API usage
|
|
77
75
|
usage_limits = create_usage_limits()
|
|
78
76
|
|
|
79
77
|
result = await run_agent(
|
|
80
78
|
agent=agent,
|
|
81
|
-
prompt=
|
|
79
|
+
prompt=prompt,
|
|
82
80
|
deps=deps,
|
|
83
81
|
message_history=message_history,
|
|
84
82
|
usage_limits=usage_limits,
|
|
83
|
+
event_stream_handler=event_stream_handler,
|
|
85
84
|
)
|
|
86
85
|
|
|
87
86
|
logger.debug("✅ Task creation completed successfully")
|