shotgun-sh 0.2.29.dev2__py3-none-any.whl → 0.6.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (161) hide show
  1. shotgun/agents/agent_manager.py +497 -30
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +90 -77
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +52 -8
  6. shotgun/agents/config/models.py +48 -45
  7. shotgun/agents/config/provider.py +44 -29
  8. shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
  9. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  10. shotgun/agents/export.py +12 -13
  11. shotgun/agents/file_read.py +176 -0
  12. shotgun/agents/messages.py +15 -3
  13. shotgun/agents/models.py +90 -2
  14. shotgun/agents/plan.py +12 -13
  15. shotgun/agents/research.py +13 -10
  16. shotgun/agents/router/__init__.py +47 -0
  17. shotgun/agents/router/models.py +384 -0
  18. shotgun/agents/router/router.py +185 -0
  19. shotgun/agents/router/tools/__init__.py +18 -0
  20. shotgun/agents/router/tools/delegation_tools.py +557 -0
  21. shotgun/agents/router/tools/plan_tools.py +403 -0
  22. shotgun/agents/runner.py +17 -2
  23. shotgun/agents/specify.py +12 -13
  24. shotgun/agents/tasks.py +12 -13
  25. shotgun/agents/tools/__init__.py +8 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  27. shotgun/agents/tools/codebase/file_read.py +26 -35
  28. shotgun/agents/tools/codebase/query_graph.py +9 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  30. shotgun/agents/tools/file_management.py +81 -3
  31. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  32. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  33. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  34. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  35. shotgun/agents/tools/markdown_tools/models.py +86 -0
  36. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  37. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  38. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  39. shotgun/agents/tools/registry.py +41 -0
  40. shotgun/agents/tools/web_search/__init__.py +1 -2
  41. shotgun/agents/tools/web_search/gemini.py +1 -3
  42. shotgun/agents/tools/web_search/openai.py +42 -23
  43. shotgun/attachments/__init__.py +41 -0
  44. shotgun/attachments/errors.py +60 -0
  45. shotgun/attachments/models.py +107 -0
  46. shotgun/attachments/parser.py +257 -0
  47. shotgun/attachments/processor.py +193 -0
  48. shotgun/cli/clear.py +2 -2
  49. shotgun/cli/codebase/commands.py +181 -65
  50. shotgun/cli/compact.py +2 -2
  51. shotgun/cli/context.py +2 -2
  52. shotgun/cli/run.py +90 -0
  53. shotgun/cli/spec/backup.py +2 -1
  54. shotgun/cli/spec/commands.py +2 -0
  55. shotgun/cli/spec/models.py +18 -0
  56. shotgun/cli/spec/pull_service.py +122 -68
  57. shotgun/codebase/__init__.py +2 -0
  58. shotgun/codebase/benchmarks/__init__.py +35 -0
  59. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  60. shotgun/codebase/benchmarks/exporters.py +119 -0
  61. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  62. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  63. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  64. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  65. shotgun/codebase/benchmarks/models.py +129 -0
  66. shotgun/codebase/core/__init__.py +4 -0
  67. shotgun/codebase/core/call_resolution.py +91 -0
  68. shotgun/codebase/core/change_detector.py +11 -6
  69. shotgun/codebase/core/errors.py +159 -0
  70. shotgun/codebase/core/extractors/__init__.py +23 -0
  71. shotgun/codebase/core/extractors/base.py +138 -0
  72. shotgun/codebase/core/extractors/factory.py +63 -0
  73. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  74. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  75. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  76. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  77. shotgun/codebase/core/extractors/protocol.py +109 -0
  78. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  79. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  80. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  81. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  82. shotgun/codebase/core/extractors/types.py +15 -0
  83. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  84. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  85. shotgun/codebase/core/gitignore.py +252 -0
  86. shotgun/codebase/core/ingestor.py +644 -354
  87. shotgun/codebase/core/kuzu_compat.py +119 -0
  88. shotgun/codebase/core/language_config.py +239 -0
  89. shotgun/codebase/core/manager.py +256 -46
  90. shotgun/codebase/core/metrics_collector.py +310 -0
  91. shotgun/codebase/core/metrics_types.py +347 -0
  92. shotgun/codebase/core/parallel_executor.py +424 -0
  93. shotgun/codebase/core/work_distributor.py +254 -0
  94. shotgun/codebase/core/worker.py +768 -0
  95. shotgun/codebase/indexing_state.py +86 -0
  96. shotgun/codebase/models.py +94 -0
  97. shotgun/codebase/service.py +13 -0
  98. shotgun/exceptions.py +1 -1
  99. shotgun/main.py +2 -10
  100. shotgun/prompts/agents/export.j2 +2 -0
  101. shotgun/prompts/agents/file_read.j2 +48 -0
  102. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
  103. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  104. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  105. shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
  106. shotgun/prompts/agents/plan.j2 +43 -1
  107. shotgun/prompts/agents/research.j2 +75 -20
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +94 -4
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -15
  112. shotgun/prompts/agents/tasks.j2 +77 -23
  113. shotgun/settings.py +44 -0
  114. shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
  115. shotgun/tui/app.py +90 -23
  116. shotgun/tui/commands/__init__.py +9 -1
  117. shotgun/tui/components/attachment_bar.py +87 -0
  118. shotgun/tui/components/mode_indicator.py +120 -25
  119. shotgun/tui/components/prompt_input.py +23 -28
  120. shotgun/tui/components/status_bar.py +5 -4
  121. shotgun/tui/dependencies.py +58 -8
  122. shotgun/tui/protocols.py +37 -0
  123. shotgun/tui/screens/chat/chat.tcss +24 -1
  124. shotgun/tui/screens/chat/chat_screen.py +1374 -211
  125. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  126. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  128. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  129. shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
  130. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  131. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  132. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  133. shotgun/tui/screens/chat_screen/messages.py +219 -0
  134. shotgun/tui/screens/database_locked_dialog.py +219 -0
  135. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  136. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  137. shotgun/tui/screens/model_picker.py +14 -9
  138. shotgun/tui/screens/models.py +11 -0
  139. shotgun/tui/screens/shotgun_auth.py +50 -0
  140. shotgun/tui/screens/spec_pull.py +2 -0
  141. shotgun/tui/state/processing_state.py +19 -0
  142. shotgun/tui/utils/mode_progress.py +20 -86
  143. shotgun/tui/widgets/__init__.py +2 -1
  144. shotgun/tui/widgets/approval_widget.py +152 -0
  145. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  146. shotgun/tui/widgets/plan_panel.py +129 -0
  147. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  148. shotgun/tui/widgets/widget_coordinator.py +18 -0
  149. shotgun/utils/file_system_utils.py +4 -1
  150. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
  151. shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
  152. shotgun/cli/export.py +0 -81
  153. shotgun/cli/plan.py +0 -73
  154. shotgun/cli/research.py +0 -93
  155. shotgun/cli/specify.py +0 -70
  156. shotgun/cli/tasks.py +0 -78
  157. shotgun/tui/screens/onboarding.py +0 -580
  158. shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
  159. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
  160. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
  161. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,403 @@
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
+ from shotgun.posthog_telemetry import track_event
29
+
30
+ logger = get_logger(__name__)
31
+
32
+
33
+ def _notify_plan_changed(deps: RouterDeps) -> None:
34
+ """Notify TUI of plan changes via callback if registered.
35
+
36
+ This helper is called after any plan modification to update the
37
+ Plan Panel widget in the TUI.
38
+
39
+ Args:
40
+ deps: RouterDeps containing the on_plan_changed callback.
41
+ """
42
+ if deps.on_plan_changed:
43
+ deps.on_plan_changed(deps.current_plan)
44
+
45
+
46
+ @register_tool(
47
+ category=ToolCategory.PLANNING,
48
+ display_text="Creating a Drafting plan",
49
+ key_arg="input",
50
+ )
51
+ async def create_plan(
52
+ ctx: RunContext[RouterDeps], input: CreatePlanInput
53
+ ) -> ToolResult:
54
+ """Create a new execution plan for the current task.
55
+
56
+ This replaces any existing plan. The plan is stored in-memory in RouterDeps,
57
+ NOT in a file. It will be shown in the system status message.
58
+
59
+ Args:
60
+ ctx: RunContext with RouterDeps
61
+ input: CreatePlanInput with goal and steps
62
+
63
+ Returns:
64
+ ToolResult indicating success or failure
65
+ """
66
+ logger.debug("Creating a Drafting plan with goal: %s", input.goal)
67
+
68
+ # Convert step inputs to ExecutionStep objects
69
+ steps = [
70
+ ExecutionStep(
71
+ id=step_input.id,
72
+ title=step_input.title,
73
+ objective=step_input.objective,
74
+ done=False,
75
+ )
76
+ for step_input in input.steps
77
+ ]
78
+
79
+ # Create and store the plan
80
+ plan = ExecutionPlan(
81
+ goal=input.goal,
82
+ steps=steps,
83
+ current_step_index=0,
84
+ )
85
+
86
+ ctx.deps.current_plan = plan
87
+
88
+ # Set pending approval for multi-step plans in Planning mode
89
+ # The TUI will detect this and show the PlanApprovalWidget
90
+ if ctx.deps.router_mode == RouterMode.PLANNING and plan.needs_approval():
91
+ ctx.deps.pending_approval = PendingApproval(plan=plan)
92
+ ctx.deps.approval_status = PlanApprovalStatus.PENDING
93
+ # Plan is NOT executing yet - user must approve first
94
+ ctx.deps.is_executing = False
95
+ logger.debug(
96
+ "Set pending approval for plan with %d steps",
97
+ len(steps),
98
+ )
99
+ else:
100
+ # Single-step plans or Drafting mode - skip approval and start executing
101
+ ctx.deps.approval_status = PlanApprovalStatus.SKIPPED
102
+ ctx.deps.is_executing = True
103
+ logger.debug("Plan approved automatically, is_executing=True")
104
+
105
+ logger.info(
106
+ "Created execution plan with %d steps: %s",
107
+ len(steps),
108
+ input.goal,
109
+ )
110
+
111
+ # Track plan creation metric
112
+ track_event(
113
+ "plan_created",
114
+ {
115
+ "step_count": len(steps),
116
+ "goal_preview": input.goal[:100],
117
+ "requires_approval": plan.needs_approval(),
118
+ "router_mode": ctx.deps.router_mode.value,
119
+ },
120
+ )
121
+
122
+ _notify_plan_changed(ctx.deps)
123
+
124
+ # Return different message based on whether approval is needed
125
+ if ctx.deps.pending_approval is not None:
126
+ return ToolResult(
127
+ success=True,
128
+ message=f"Created plan with {len(steps)} steps. Goal: {input.goal}\n\n"
129
+ "IMPORTANT: This plan requires user approval before execution. "
130
+ "You MUST call final_result NOW to present this plan to the user. "
131
+ "Do NOT attempt to delegate or execute any steps yet.",
132
+ )
133
+
134
+ return ToolResult(
135
+ success=True,
136
+ message=f"Created plan with {len(steps)} steps. Goal: {input.goal}",
137
+ )
138
+
139
+
140
+ @register_tool(
141
+ category=ToolCategory.PLANNING,
142
+ display_text="Marking step complete",
143
+ key_arg="input",
144
+ )
145
+ async def mark_step_done(
146
+ ctx: RunContext[RouterDeps], input: MarkStepDoneInput
147
+ ) -> ToolResult:
148
+ """Mark a step in the execution plan as complete.
149
+
150
+ Args:
151
+ ctx: RunContext with RouterDeps
152
+ input: MarkStepDoneInput with step_id
153
+
154
+ Returns:
155
+ ToolResult indicating success or failure
156
+ """
157
+ plan = ctx.deps.current_plan
158
+
159
+ if plan is None:
160
+ return ToolResult(
161
+ success=False,
162
+ message="No execution plan exists. Create a plan first.",
163
+ )
164
+
165
+ # Find the step by ID
166
+ for step_index, step in enumerate(plan.steps):
167
+ if step.id == input.step_id:
168
+ step.done = True
169
+ logger.info("Marked step '%s' as done", input.step_id)
170
+
171
+ # Track step completion metric
172
+ completed_count = sum(1 for s in plan.steps if s.done)
173
+ track_event(
174
+ "plan_step_completed",
175
+ {
176
+ "step_position": step_index + 1, # 1-indexed for human readability
177
+ "total_steps": len(plan.steps),
178
+ "steps_remaining": len(plan.steps) - completed_count,
179
+ },
180
+ )
181
+
182
+ # Advance current_step_index to next incomplete step
183
+ while (
184
+ plan.current_step_index < len(plan.steps)
185
+ and plan.steps[plan.current_step_index].done
186
+ ):
187
+ plan.current_step_index += 1
188
+
189
+ # Check if plan is complete
190
+ if plan.is_complete():
191
+ ctx.deps.is_executing = False
192
+ logger.debug("Plan complete, is_executing=False")
193
+
194
+ # Track plan completion metric
195
+ track_event(
196
+ "plan_completed",
197
+ {
198
+ "step_count": len(plan.steps),
199
+ },
200
+ )
201
+
202
+ # Set pending completion for Drafting mode
203
+ # The TUI will detect this and show the completion message
204
+ if ctx.deps.router_mode == RouterMode.DRAFTING:
205
+ ctx.deps.pending_completion = True
206
+ logger.debug("Set pending_completion=True for drafting mode")
207
+ # Set pending checkpoint for Planning mode
208
+ # The TUI will detect this and show the StepCheckpointWidget
209
+ elif ctx.deps.router_mode == RouterMode.PLANNING:
210
+ # Use current_step() since the while loop above already advanced
211
+ # current_step_index to the next incomplete step
212
+ next_step = plan.current_step()
213
+ ctx.deps.pending_checkpoint = PendingCheckpoint(
214
+ completed_step=step, next_step=next_step
215
+ )
216
+ logger.debug(
217
+ "Set pending checkpoint: completed='%s', next='%s'",
218
+ step.title,
219
+ next_step.title if next_step else None,
220
+ )
221
+
222
+ _notify_plan_changed(ctx.deps)
223
+
224
+ # Return different messages based on mode and plan state
225
+ if plan.is_complete():
226
+ return ToolResult(
227
+ success=True,
228
+ message=f"Marked step '{step.title}' as complete.\n\n"
229
+ "All plan steps are now complete. You may return your final response.",
230
+ )
231
+ elif ctx.deps.router_mode == RouterMode.DRAFTING:
232
+ next_step = plan.current_step()
233
+ return ToolResult(
234
+ success=True,
235
+ message=f"Marked step '{step.title}' as complete.\n\n"
236
+ f"NEXT STEP: {next_step.title if next_step else 'None'}\n"
237
+ "IMPORTANT: Do NOT call final_result. "
238
+ "Immediately delegate the next step.",
239
+ )
240
+ else:
241
+ # Planning mode - checkpoint will be shown
242
+ return ToolResult(
243
+ success=True,
244
+ message=f"Marked step '{step.title}' as complete.",
245
+ )
246
+
247
+ return ToolResult(
248
+ success=False,
249
+ message=f"Step with ID '{input.step_id}' not found in plan.",
250
+ )
251
+
252
+
253
+ @register_tool(
254
+ category=ToolCategory.PLANNING,
255
+ display_text="Adding step to plan",
256
+ key_arg="input",
257
+ )
258
+ async def add_step(ctx: RunContext[RouterDeps], input: AddStepInput) -> ToolResult:
259
+ """Add a new step to the execution plan.
260
+
261
+ The step can be inserted after a specific step (by ID) or appended to the end.
262
+
263
+ Args:
264
+ ctx: RunContext with RouterDeps
265
+ input: AddStepInput with step details and optional after_step_id
266
+
267
+ Returns:
268
+ ToolResult indicating success or failure
269
+ """
270
+ plan = ctx.deps.current_plan
271
+
272
+ if plan is None:
273
+ return ToolResult(
274
+ success=False,
275
+ message="No execution plan exists. Create a plan first.",
276
+ )
277
+
278
+ # Check for duplicate ID
279
+ existing_ids = {step.id for step in plan.steps}
280
+ if input.step.id in existing_ids:
281
+ return ToolResult(
282
+ success=False,
283
+ message=f"Step with ID '{input.step.id}' already exists in plan.",
284
+ )
285
+
286
+ # Create the new step
287
+ new_step = ExecutionStep(
288
+ id=input.step.id,
289
+ title=input.step.title,
290
+ objective=input.step.objective,
291
+ done=False,
292
+ )
293
+
294
+ # Insert at the specified position
295
+ if input.after_step_id is None:
296
+ # Append to end
297
+ plan.steps.append(new_step)
298
+ logger.info("Appended step '%s' to end of plan", input.step.id)
299
+
300
+ # Track step added metric
301
+ track_event(
302
+ "plan_step_added",
303
+ {
304
+ "new_step_count": len(plan.steps),
305
+ "position": len(plan.steps), # Appended at end
306
+ },
307
+ )
308
+
309
+ _notify_plan_changed(ctx.deps)
310
+
311
+ return ToolResult(
312
+ success=True,
313
+ message=f"Added step '{new_step.title}' at end of plan.",
314
+ )
315
+
316
+ # Find the position to insert after
317
+ for i, step in enumerate(plan.steps):
318
+ if step.id == input.after_step_id:
319
+ plan.steps.insert(i + 1, new_step)
320
+ logger.info(
321
+ "Inserted step '%s' after '%s'",
322
+ input.step.id,
323
+ input.after_step_id,
324
+ )
325
+
326
+ # Track step added metric
327
+ track_event(
328
+ "plan_step_added",
329
+ {
330
+ "new_step_count": len(plan.steps),
331
+ "position": i + 2, # 1-indexed, inserted after position i+1
332
+ },
333
+ )
334
+
335
+ _notify_plan_changed(ctx.deps)
336
+
337
+ return ToolResult(
338
+ success=True,
339
+ message=f"Added step '{new_step.title}' after '{step.title}'.",
340
+ )
341
+
342
+ return ToolResult(
343
+ success=False,
344
+ message=f"Step with ID '{input.after_step_id}' not found in plan.",
345
+ )
346
+
347
+
348
+ @register_tool(
349
+ category=ToolCategory.PLANNING,
350
+ display_text="Removing step from plan",
351
+ key_arg="input",
352
+ )
353
+ async def remove_step(
354
+ ctx: RunContext[RouterDeps], input: RemoveStepInput
355
+ ) -> ToolResult:
356
+ """Remove a step from the execution plan.
357
+
358
+ Args:
359
+ ctx: RunContext with RouterDeps
360
+ input: RemoveStepInput with step_id
361
+
362
+ Returns:
363
+ ToolResult indicating success or failure
364
+ """
365
+ plan = ctx.deps.current_plan
366
+
367
+ if plan is None:
368
+ return ToolResult(
369
+ success=False,
370
+ message="No execution plan exists. Create a plan first.",
371
+ )
372
+
373
+ # Find and remove the step
374
+ for i, step in enumerate(plan.steps):
375
+ if step.id == input.step_id:
376
+ removed_step = plan.steps.pop(i)
377
+ logger.info("Removed step '%s' from plan", input.step_id)
378
+
379
+ # Track step removed metric
380
+ track_event(
381
+ "plan_step_removed",
382
+ {
383
+ "new_step_count": len(plan.steps),
384
+ },
385
+ )
386
+
387
+ # Adjust current_step_index if needed
388
+ if plan.current_step_index > i:
389
+ plan.current_step_index -= 1
390
+ elif plan.current_step_index >= len(plan.steps):
391
+ plan.current_step_index = max(0, len(plan.steps) - 1)
392
+
393
+ _notify_plan_changed(ctx.deps)
394
+
395
+ return ToolResult(
396
+ success=True,
397
+ message=f"Removed step '{removed_step.title}' from plan.",
398
+ )
399
+
400
+ return ToolResult(
401
+ success=False,
402
+ message=f"Step with ID '{input.step_id}' not found in plan.",
403
+ )
shotgun/agents/runner.py CHANGED
@@ -13,6 +13,7 @@ from openai import APIStatusError as OpenAIAPIStatusError
13
13
  from pydantic_ai.exceptions import ModelHTTPError, UnexpectedModelBehavior
14
14
 
15
15
  from shotgun.agents.error.models import AgentErrorContext
16
+ from shotgun.attachments import FileAttachment
16
17
  from shotgun.exceptions import (
17
18
  AgentCancelledException,
18
19
  BudgetExceededException,
@@ -29,6 +30,8 @@ from shotgun.exceptions import (
29
30
  )
30
31
 
31
32
  if TYPE_CHECKING:
33
+ from pydantic_ai import BinaryContent
34
+
32
35
  from shotgun.agents.agent_manager import AgentManager
33
36
 
34
37
  logger = logging.getLogger(__name__)
@@ -66,11 +69,19 @@ class AgentRunner:
66
69
  """
67
70
  self.agent_manager = agent_manager
68
71
 
69
- async def run(self, prompt: str) -> None:
72
+ async def run(
73
+ self,
74
+ prompt: str,
75
+ attachment: FileAttachment | None = None,
76
+ file_contents: list[tuple[str, "BinaryContent"]] | None = None,
77
+ ) -> None:
70
78
  """Run the agent with the given prompt.
71
79
 
72
80
  Args:
73
81
  prompt: The user's prompt/query
82
+ attachment: Optional file attachment to include as multimodal content.
83
+ file_contents: Optional list of (file_path, BinaryContent) tuples to include
84
+ as multimodal content. Used when resuming after file_requests.
74
85
 
75
86
  Raises:
76
87
  Custom exceptions for different error types:
@@ -88,7 +99,11 @@ class AgentRunner:
88
99
  - UnknownAgentException: Unknown/unclassified error
89
100
  """
90
101
  try:
91
- await self.agent_manager.run(prompt=prompt)
102
+ await self.agent_manager.run(
103
+ prompt=prompt,
104
+ attachment=attachment,
105
+ file_contents=file_contents,
106
+ )
92
107
 
93
108
  except asyncio.CancelledError as e:
94
109
  # User cancelled - wrap and re-raise as our custom 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")
@@ -8,6 +8,11 @@ from .codebase import (
8
8
  retrieve_code,
9
9
  )
10
10
  from .file_management import append_file, read_file, write_file
11
+ from .markdown_tools import (
12
+ insert_markdown_section,
13
+ remove_markdown_section,
14
+ replace_markdown_section,
15
+ )
11
16
  from .web_search import (
12
17
  anthropic_web_search_tool,
13
18
  gemini_web_search_tool,
@@ -23,6 +28,9 @@ __all__ = [
23
28
  "read_file",
24
29
  "write_file",
25
30
  "append_file",
31
+ "replace_markdown_section",
32
+ "insert_markdown_section",
33
+ "remove_markdown_section",
26
34
  # Codebase understanding tools
27
35
  "query_graph",
28
36
  "retrieve_code",