shotgun-sh 0.2.17__py3-none-any.whl → 0.4.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. shotgun/agents/agent_manager.py +219 -37
  2. shotgun/agents/common.py +79 -78
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/manager.py +364 -53
  6. shotgun/agents/config/models.py +101 -21
  7. shotgun/agents/config/provider.py +51 -13
  8. shotgun/agents/config/streaming_test.py +119 -0
  9. shotgun/agents/context_analyzer/analyzer.py +6 -2
  10. shotgun/agents/conversation/__init__.py +18 -0
  11. shotgun/agents/conversation/filters.py +164 -0
  12. shotgun/agents/conversation/history/chunking.py +278 -0
  13. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  14. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  15. shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
  16. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  17. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
  18. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  19. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  20. shotgun/agents/error/__init__.py +11 -0
  21. shotgun/agents/error/models.py +19 -0
  22. shotgun/agents/export.py +12 -13
  23. shotgun/agents/models.py +66 -1
  24. shotgun/agents/plan.py +12 -13
  25. shotgun/agents/research.py +13 -10
  26. shotgun/agents/router/__init__.py +47 -0
  27. shotgun/agents/router/models.py +376 -0
  28. shotgun/agents/router/router.py +185 -0
  29. shotgun/agents/router/tools/__init__.py +18 -0
  30. shotgun/agents/router/tools/delegation_tools.py +503 -0
  31. shotgun/agents/router/tools/plan_tools.py +322 -0
  32. shotgun/agents/runner.py +230 -0
  33. shotgun/agents/specify.py +12 -13
  34. shotgun/agents/tasks.py +12 -13
  35. shotgun/agents/tools/file_management.py +49 -1
  36. shotgun/agents/tools/registry.py +2 -0
  37. shotgun/agents/tools/web_search/__init__.py +1 -2
  38. shotgun/agents/tools/web_search/gemini.py +1 -3
  39. shotgun/agents/tools/web_search/openai.py +1 -1
  40. shotgun/build_constants.py +2 -2
  41. shotgun/cli/clear.py +1 -1
  42. shotgun/cli/compact.py +5 -3
  43. shotgun/cli/context.py +44 -1
  44. shotgun/cli/error_handler.py +24 -0
  45. shotgun/cli/export.py +34 -34
  46. shotgun/cli/plan.py +34 -34
  47. shotgun/cli/research.py +17 -9
  48. shotgun/cli/spec/__init__.py +5 -0
  49. shotgun/cli/spec/backup.py +81 -0
  50. shotgun/cli/spec/commands.py +132 -0
  51. shotgun/cli/spec/models.py +48 -0
  52. shotgun/cli/spec/pull_service.py +219 -0
  53. shotgun/cli/specify.py +20 -19
  54. shotgun/cli/tasks.py +34 -34
  55. shotgun/codebase/core/change_detector.py +1 -1
  56. shotgun/codebase/core/ingestor.py +154 -8
  57. shotgun/codebase/core/manager.py +1 -1
  58. shotgun/codebase/models.py +2 -0
  59. shotgun/exceptions.py +325 -0
  60. shotgun/llm_proxy/__init__.py +17 -0
  61. shotgun/llm_proxy/client.py +215 -0
  62. shotgun/llm_proxy/models.py +137 -0
  63. shotgun/logging_config.py +42 -0
  64. shotgun/main.py +4 -0
  65. shotgun/posthog_telemetry.py +1 -1
  66. shotgun/prompts/agents/export.j2 +2 -0
  67. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
  68. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  69. shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
  70. shotgun/prompts/agents/plan.j2 +29 -1
  71. shotgun/prompts/agents/research.j2 +75 -23
  72. shotgun/prompts/agents/router.j2 +440 -0
  73. shotgun/prompts/agents/specify.j2 +80 -4
  74. shotgun/prompts/agents/state/system_state.j2 +15 -8
  75. shotgun/prompts/agents/tasks.j2 +63 -23
  76. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  77. shotgun/prompts/history/combine_summaries.j2 +53 -0
  78. shotgun/sdk/codebase.py +14 -3
  79. shotgun/settings.py +5 -0
  80. shotgun/shotgun_web/__init__.py +67 -1
  81. shotgun/shotgun_web/client.py +42 -1
  82. shotgun/shotgun_web/constants.py +46 -0
  83. shotgun/shotgun_web/exceptions.py +29 -0
  84. shotgun/shotgun_web/models.py +390 -0
  85. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  86. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  87. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  88. shotgun/shotgun_web/shared_specs/models.py +71 -0
  89. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  90. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  91. shotgun/shotgun_web/specs_client.py +703 -0
  92. shotgun/shotgun_web/supabase_client.py +31 -0
  93. shotgun/tui/app.py +78 -15
  94. shotgun/tui/components/mode_indicator.py +120 -25
  95. shotgun/tui/components/status_bar.py +2 -2
  96. shotgun/tui/containers.py +1 -1
  97. shotgun/tui/dependencies.py +64 -9
  98. shotgun/tui/layout.py +5 -0
  99. shotgun/tui/protocols.py +37 -0
  100. shotgun/tui/screens/chat/chat.tcss +9 -1
  101. shotgun/tui/screens/chat/chat_screen.py +1015 -106
  102. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
  103. shotgun/tui/screens/chat_screen/command_providers.py +13 -89
  104. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  105. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  106. shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
  107. shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
  108. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  109. shotgun/tui/screens/chat_screen/messages.py +219 -0
  110. shotgun/tui/screens/confirmation_dialog.py +40 -0
  111. shotgun/tui/screens/directory_setup.py +45 -41
  112. shotgun/tui/screens/feedback.py +10 -3
  113. shotgun/tui/screens/github_issue.py +11 -2
  114. shotgun/tui/screens/model_picker.py +28 -8
  115. shotgun/tui/screens/onboarding.py +179 -26
  116. shotgun/tui/screens/pipx_migration.py +58 -6
  117. shotgun/tui/screens/provider_config.py +66 -8
  118. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  119. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  120. shotgun/tui/screens/shared_specs/models.py +56 -0
  121. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  122. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  123. shotgun/tui/screens/shotgun_auth.py +110 -16
  124. shotgun/tui/screens/spec_pull.py +288 -0
  125. shotgun/tui/screens/welcome.py +123 -0
  126. shotgun/tui/services/conversation_service.py +5 -2
  127. shotgun/tui/utils/mode_progress.py +20 -86
  128. shotgun/tui/widgets/__init__.py +2 -1
  129. shotgun/tui/widgets/approval_widget.py +152 -0
  130. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  131. shotgun/tui/widgets/plan_panel.py +129 -0
  132. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  133. shotgun/tui/widgets/widget_coordinator.py +1 -1
  134. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
  135. shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
  136. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
  137. shotgun_sh-0.2.17.dist-info/RECORD +0 -194
  138. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  139. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  140. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  141. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  142. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  143. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  144. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  145. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  146. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  147. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  148. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  149. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
  150. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -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
+ )
@@ -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[Agent[AgentDeps, AgentResponse], AgentDeps]:
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: Agent[AgentDeps, AgentResponse],
55
- requirement: str,
53
+ agent: ShotgunAgent,
54
+ prompt: str,
56
55
  deps: AgentDeps,
57
56
  message_history: list[ModelMessage] | None = None,
57
+ event_stream_handler: EventStreamHandler | None = None,
58
58
  ) -> AgentRunResult[AgentResponse]:
59
- """Create or update specifications based on the given requirement.
59
+ """Create or update specifications based on the given prompt.
60
60
 
61
61
  Args:
62
62
  agent: The configured specify agent
63
- requirement: The specification requirement or instruction
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 requirement: %s", requirement)
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=full_prompt,
81
+ prompt=prompt,
84
82
  deps=deps,
85
83
  message_history=message_history,
86
84
  usage_limits=usage_limits,
85
+ event_stream_handler=event_stream_handler,
87
86
  )
88
87
 
89
88
  logger.debug("✅ 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[Agent[AgentDeps, AgentResponse], AgentDeps]:
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: Agent[AgentDeps, AgentResponse],
53
- instruction: str,
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 instruction.
57
+ """Create or update tasks based on the given prompt.
58
58
 
59
59
  Args:
60
60
  agent: The configured tasks agent
61
- instruction: The task creation/update instruction
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 instruction: %s", instruction)
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=full_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")