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
@@ -17,10 +17,9 @@ from tenacity import (
17
17
  )
18
18
 
19
19
  if TYPE_CHECKING:
20
- from shotgun.agents.conversation_history import ConversationState
20
+ from shotgun.agents.conversation import ConversationState
21
21
 
22
22
  from pydantic_ai import (
23
- Agent,
24
23
  RunContext,
25
24
  UsageLimits,
26
25
  )
@@ -38,6 +37,7 @@ from pydantic_ai.messages import (
38
37
  PartDeltaEvent,
39
38
  PartStartEvent,
40
39
  SystemPromptPart,
40
+ TextPartDelta,
41
41
  ToolCallPart,
42
42
  ToolCallPartDelta,
43
43
  UserPromptPart,
@@ -61,19 +61,24 @@ from shotgun.agents.context_analyzer import (
61
61
  from shotgun.agents.models import (
62
62
  AgentResponse,
63
63
  AgentType,
64
+ AnyAgent,
64
65
  FileOperation,
65
66
  FileOperationTracker,
67
+ RouterAgent,
68
+ ShotgunAgent,
66
69
  )
67
70
  from shotgun.posthog_telemetry import track_event
68
71
  from shotgun.tui.screens.chat_screen.hint_message import HintMessage
69
72
  from shotgun.utils.source_detection import detect_source
70
73
 
74
+ from .conversation.history.compaction import apply_persistent_compaction
71
75
  from .export import create_export_agent
72
- from .history.compaction import apply_persistent_compaction
73
76
  from .messages import AgentSystemPrompt
74
77
  from .models import AgentDeps, AgentRuntimeOptions
75
78
  from .plan import create_plan_agent
76
79
  from .research import create_research_agent
80
+ from .router import create_router_agent
81
+ from .router.models import RouterDeps
77
82
  from .specify import create_specify_agent
78
83
  from .tasks import create_tasks_agent
79
84
 
@@ -174,6 +179,67 @@ class CompactionCompletedMessage(Message):
174
179
  """Event posted when conversation compaction completes."""
175
180
 
176
181
 
182
+ class ToolExecutionStartedMessage(Message):
183
+ """Event posted when a tool starts executing.
184
+
185
+ This allows the UI to update the spinner text to provide feedback
186
+ during long-running tool executions.
187
+ """
188
+
189
+ def __init__(self, spinner_text: str = "Processing...") -> None:
190
+ """Initialize the tool execution started message.
191
+
192
+ Args:
193
+ spinner_text: The spinner message to display
194
+ """
195
+ super().__init__()
196
+ self.spinner_text = spinner_text
197
+
198
+
199
+ class ToolStreamingProgressMessage(Message):
200
+ """Event posted during tool call streaming to show progress.
201
+
202
+ This provides visual feedback while tool arguments are streaming,
203
+ especially useful for long-running writes like file content.
204
+ """
205
+
206
+ def __init__(self, streamed_tokens: int, spinner_text: str) -> None:
207
+ """Initialize the tool streaming progress message.
208
+
209
+ Args:
210
+ streamed_tokens: Approximate number of tokens streamed so far
211
+ spinner_text: The current spinner message to preserve
212
+ """
213
+ super().__init__()
214
+ self.streamed_tokens = streamed_tokens
215
+ self.spinner_text = spinner_text
216
+
217
+
218
+ # Fun spinner messages to show during tool execution
219
+ SPINNER_MESSAGES = [
220
+ "Pontificating...",
221
+ "Ruminating...",
222
+ "Cogitating...",
223
+ "Deliberating...",
224
+ "Contemplating...",
225
+ "Reticulating splines...",
226
+ "Consulting the oracle...",
227
+ "Gathering thoughts...",
228
+ "Processing neurons...",
229
+ "Summoning wisdom...",
230
+ "Brewing ideas...",
231
+ "Polishing pixels...",
232
+ "Herding electrons...",
233
+ "Warming up the flux capacitor...",
234
+ "Consulting ancient tomes...",
235
+ "Channeling the muses...",
236
+ "Percolating possibilities...",
237
+ "Untangling complexity...",
238
+ "Shuffling priorities...",
239
+ "Aligning the stars...",
240
+ ]
241
+
242
+
177
243
  class AgentStreamingStarted(Message):
178
244
  """Event posted when agent starts streaming responses."""
179
245
 
@@ -210,6 +276,11 @@ class _PartialStreamState:
210
276
 
211
277
  messages: list[ModelRequest | ModelResponse] = field(default_factory=list)
212
278
  current_response: ModelResponse | None = None
279
+ # Token counting for tool call streaming progress
280
+ streamed_tokens: int = 0
281
+ current_spinner_text: str = "Processing..."
282
+ # Track last reported tokens to throttle UI updates
283
+ last_reported_tokens: int = 0
213
284
 
214
285
 
215
286
  class AgentManager(Widget):
@@ -245,16 +316,18 @@ class AgentManager(Widget):
245
316
  )
246
317
 
247
318
  # Lazy initialization - agents created on first access
248
- self._research_agent: Agent[AgentDeps, AgentResponse] | None = None
319
+ self._research_agent: ShotgunAgent | None = None
249
320
  self._research_deps: AgentDeps | None = None
250
- self._plan_agent: Agent[AgentDeps, AgentResponse] | None = None
321
+ self._plan_agent: ShotgunAgent | None = None
251
322
  self._plan_deps: AgentDeps | None = None
252
- self._tasks_agent: Agent[AgentDeps, AgentResponse] | None = None
323
+ self._tasks_agent: ShotgunAgent | None = None
253
324
  self._tasks_deps: AgentDeps | None = None
254
- self._specify_agent: Agent[AgentDeps, AgentResponse] | None = None
325
+ self._specify_agent: ShotgunAgent | None = None
255
326
  self._specify_deps: AgentDeps | None = None
256
- self._export_agent: Agent[AgentDeps, AgentResponse] | None = None
327
+ self._export_agent: ShotgunAgent | None = None
257
328
  self._export_deps: AgentDeps | None = None
329
+ self._router_agent: RouterAgent | None = None
330
+ self._router_deps: RouterDeps | None = None
258
331
  self._agents_initialized = False
259
332
 
260
333
  # Track current active agent
@@ -291,10 +364,13 @@ class AgentManager(Widget):
291
364
  self._export_agent, self._export_deps = await create_export_agent(
292
365
  agent_runtime_options=self._agent_runtime_options
293
366
  )
367
+ self._router_agent, self._router_deps = await create_router_agent(
368
+ agent_runtime_options=self._agent_runtime_options
369
+ )
294
370
  self._agents_initialized = True
295
371
 
296
372
  @property
297
- def research_agent(self) -> Agent[AgentDeps, AgentResponse]:
373
+ def research_agent(self) -> ShotgunAgent:
298
374
  """Get research agent (must call _ensure_agents_initialized first)."""
299
375
  if self._research_agent is None:
300
376
  raise RuntimeError(
@@ -312,7 +388,7 @@ class AgentManager(Widget):
312
388
  return self._research_deps
313
389
 
314
390
  @property
315
- def plan_agent(self) -> Agent[AgentDeps, AgentResponse]:
391
+ def plan_agent(self) -> ShotgunAgent:
316
392
  """Get plan agent (must call _ensure_agents_initialized first)."""
317
393
  if self._plan_agent is None:
318
394
  raise RuntimeError(
@@ -330,7 +406,7 @@ class AgentManager(Widget):
330
406
  return self._plan_deps
331
407
 
332
408
  @property
333
- def tasks_agent(self) -> Agent[AgentDeps, AgentResponse]:
409
+ def tasks_agent(self) -> ShotgunAgent:
334
410
  """Get tasks agent (must call _ensure_agents_initialized first)."""
335
411
  if self._tasks_agent is None:
336
412
  raise RuntimeError(
@@ -348,7 +424,7 @@ class AgentManager(Widget):
348
424
  return self._tasks_deps
349
425
 
350
426
  @property
351
- def specify_agent(self) -> Agent[AgentDeps, AgentResponse]:
427
+ def specify_agent(self) -> ShotgunAgent:
352
428
  """Get specify agent (must call _ensure_agents_initialized first)."""
353
429
  if self._specify_agent is None:
354
430
  raise RuntimeError(
@@ -366,7 +442,7 @@ class AgentManager(Widget):
366
442
  return self._specify_deps
367
443
 
368
444
  @property
369
- def export_agent(self) -> Agent[AgentDeps, AgentResponse]:
445
+ def export_agent(self) -> ShotgunAgent:
370
446
  """Get export agent (must call _ensure_agents_initialized first)."""
371
447
  if self._export_agent is None:
372
448
  raise RuntimeError(
@@ -384,29 +460,48 @@ class AgentManager(Widget):
384
460
  return self._export_deps
385
461
 
386
462
  @property
387
- def current_agent(self) -> Agent[AgentDeps, AgentResponse]:
463
+ def router_agent(self) -> RouterAgent:
464
+ """Get router agent (must call _ensure_agents_initialized first)."""
465
+ if self._router_agent is None:
466
+ raise RuntimeError(
467
+ "Agents not initialized. Call _ensure_agents_initialized() first."
468
+ )
469
+ return self._router_agent
470
+
471
+ @property
472
+ def router_deps(self) -> RouterDeps:
473
+ """Get router deps (must call _ensure_agents_initialized first)."""
474
+ if self._router_deps is None:
475
+ raise RuntimeError(
476
+ "Agents not initialized. Call _ensure_agents_initialized() first."
477
+ )
478
+ return self._router_deps
479
+
480
+ @property
481
+ def current_agent(self) -> AnyAgent:
388
482
  """Get the currently active agent.
389
483
 
390
484
  Returns:
391
- The currently selected agent instance.
485
+ The currently selected agent instance (ShotgunAgent or RouterAgent).
392
486
  """
393
487
  return self._get_agent(self._current_agent_type)
394
488
 
395
- def _get_agent(self, agent_type: AgentType) -> Agent[AgentDeps, AgentResponse]:
489
+ def _get_agent(self, agent_type: AgentType) -> AnyAgent:
396
490
  """Get agent by type.
397
491
 
398
492
  Args:
399
493
  agent_type: The type of agent to retrieve.
400
494
 
401
495
  Returns:
402
- The requested agent instance.
496
+ The requested agent instance (ShotgunAgent or RouterAgent).
403
497
  """
404
- agent_map = {
498
+ agent_map: dict[AgentType, AnyAgent] = {
405
499
  AgentType.RESEARCH: self.research_agent,
406
500
  AgentType.PLAN: self.plan_agent,
407
501
  AgentType.TASKS: self.tasks_agent,
408
502
  AgentType.SPECIFY: self.specify_agent,
409
503
  AgentType.EXPORT: self.export_agent,
504
+ AgentType.ROUTER: self.router_agent,
410
505
  }
411
506
  return agent_map[agent_type]
412
507
 
@@ -419,12 +514,13 @@ class AgentManager(Widget):
419
514
  Returns:
420
515
  The agent-specific dependencies.
421
516
  """
422
- deps_map = {
517
+ deps_map: dict[AgentType, AgentDeps] = {
423
518
  AgentType.RESEARCH: self.research_deps,
424
519
  AgentType.PLAN: self.plan_deps,
425
520
  AgentType.TASKS: self.tasks_deps,
426
521
  AgentType.SPECIFY: self.specify_deps,
427
522
  AgentType.EXPORT: self.export_deps,
523
+ AgentType.ROUTER: self.router_deps,
428
524
  }
429
525
  return deps_map[agent_type]
430
526
 
@@ -433,6 +529,10 @@ class AgentManager(Widget):
433
529
 
434
530
  This preserves the agent's system_prompt_fn while using shared runtime state.
435
531
 
532
+ For Router agent, returns the shared deps directly (not a copy) because
533
+ Router state (pending_approval, current_plan, etc.) must be shared with
534
+ the TUI for features like plan approval widgets.
535
+
436
536
  Args:
437
537
  agent_type: The type of agent to create merged deps for.
438
538
 
@@ -445,8 +545,14 @@ class AgentManager(Widget):
445
545
  if self.deps is None:
446
546
  raise ValueError("Shared deps is None - this should not happen")
447
547
 
448
- # Create new deps with shared runtime state but agent's system_prompt_fn
449
- # Use a copy of the shared deps and update the system_prompt_fn
548
+ # For Router, use shared deps directly so state mutations are visible to TUI
549
+ # (e.g., pending_approval, current_plan need to be seen by ChatScreen)
550
+ if agent_type == AgentType.ROUTER:
551
+ # Update system_prompt_fn on shared deps in place
552
+ self.deps.system_prompt_fn = agent_deps.system_prompt_fn
553
+ return self.deps
554
+
555
+ # For other agents, create a copy with agent-specific system_prompt_fn
450
556
  merged_deps = self.deps.model_copy(
451
557
  update={"system_prompt_fn": agent_deps.system_prompt_fn}
452
558
  )
@@ -478,7 +584,7 @@ class AgentManager(Widget):
478
584
  )
479
585
  async def _run_agent_with_retry(
480
586
  self,
481
- agent: Agent[AgentDeps, AgentResponse],
587
+ agent: AnyAgent,
482
588
  prompt: str | None,
483
589
  deps: AgentDeps,
484
590
  usage_limits: UsageLimits | None,
@@ -489,9 +595,9 @@ class AgentManager(Widget):
489
595
  """Run agent with automatic retry on transient errors.
490
596
 
491
597
  Args:
492
- agent: The agent to run.
598
+ agent: The agent to run (ShotgunAgent or RouterAgent).
493
599
  prompt: Optional prompt to send to the agent.
494
- deps: Agent dependencies.
600
+ deps: Agent dependencies (AgentDeps or RouterDeps).
495
601
  usage_limits: Optional usage limits.
496
602
  message_history: Message history to provide to agent.
497
603
  event_stream_handler: Event handler for streaming.
@@ -502,8 +608,16 @@ class AgentManager(Widget):
502
608
 
503
609
  Raises:
504
610
  Various exceptions if all retries fail.
611
+
612
+ Note:
613
+ Type safety for agent/deps pairing is maintained by AgentManager's
614
+ _get_agent_deps which ensures the correct deps type is used for each
615
+ agent type. The cast is needed because Agent is contravariant in deps.
505
616
  """
506
- return await agent.run(
617
+ # Cast needed because Agent is contravariant in deps type parameter.
618
+ # The agent/deps pairing is ensured by _get_agent_deps returning the
619
+ # correct deps type for each agent type.
620
+ return await cast(ShotgunAgent, agent).run(
507
621
  prompt,
508
622
  deps=deps,
509
623
  usage_limits=usage_limits,
@@ -560,6 +674,11 @@ class AgentManager(Widget):
560
674
 
561
675
  deps.agent_mode = self._current_agent_type
562
676
 
677
+ # For router agent, set up the parent stream handler so sub-agents can stream
678
+ if self._current_agent_type == AgentType.ROUTER:
679
+ if isinstance(deps, RouterDeps):
680
+ deps.parent_stream_handler = self._handle_event_stream # type: ignore[assignment]
681
+
563
682
  # Filter out system prompts from other agent types
564
683
  from pydantic_ai.messages import ModelRequestPart
565
684
 
@@ -615,19 +734,33 @@ class AgentManager(Widget):
615
734
  self._stream_state = _PartialStreamState()
616
735
 
617
736
  model_name = ""
737
+ supports_streaming = True # Default to streaming enabled
738
+
618
739
  if hasattr(deps, "llm_model") and deps.llm_model is not None:
619
740
  model_name = deps.llm_model.name
741
+ supports_streaming = deps.llm_model.supports_streaming
620
742
 
621
- # Check if it's a Shotgun account
622
- is_shotgun_account = (
623
- hasattr(deps, "llm_model")
624
- and deps.llm_model is not None
625
- and deps.llm_model.key_provider == KeyProvider.SHOTGUN
626
- )
627
-
628
- # Only disable streaming for GPT-5 if NOT a Shotgun account
629
- # Shotgun accounts support streaming for GPT-5
630
- is_gpt5_byok = "gpt-5" in model_name.lower() and not is_shotgun_account
743
+ # Add hint message if streaming is disabled for BYOK GPT-5 models
744
+ if (
745
+ not supports_streaming
746
+ and deps.llm_model.key_provider == KeyProvider.BYOK
747
+ ):
748
+ self.ui_message_history.append(
749
+ HintMessage(
750
+ message=(
751
+ "⚠️ **Streaming not available for GPT-5**\n\n"
752
+ "Your OpenAI organization doesn't have streaming enabled for this model.\n\n"
753
+ "**Options:**\n"
754
+ "- Get a [Shotgun Account](https://shotgun.sh) - streaming works out of the box\n"
755
+ "- Complete [Biometric Verification](https://platform.openai.com/settings/organization/general) with OpenAI, then:\n"
756
+ " 1. Press `Ctrl+P` → Open Provider Setup\n"
757
+ " 2. Select OpenAI → Clear key\n"
758
+ " 3. Re-add your OpenAI API key\n\n"
759
+ "Continuing without streaming (responses will appear all at once)."
760
+ )
761
+ )
762
+ )
763
+ self._post_messages_updated()
631
764
 
632
765
  # Track message send event
633
766
  event_name = f"message_send_{self._current_agent_type.value}"
@@ -647,7 +780,7 @@ class AgentManager(Widget):
647
780
  usage_limits=usage_limits,
648
781
  message_history=message_history,
649
782
  event_stream_handler=self._handle_event_stream
650
- if not is_gpt5_byok
783
+ if supports_streaming
651
784
  else None,
652
785
  **kwargs,
653
786
  )
@@ -978,6 +1111,44 @@ class AgentManager(Widget):
978
1111
  )
979
1112
  continue
980
1113
 
1114
+ # Count tokens from the delta for progress indication
1115
+ delta_len = 0
1116
+ is_tool_call_delta = False
1117
+ if isinstance(event.delta, ToolCallPartDelta):
1118
+ is_tool_call_delta = True
1119
+ # args_delta can be str or dict depending on provider
1120
+ args_delta = event.delta.args_delta
1121
+ if isinstance(args_delta, str):
1122
+ delta_len = len(args_delta)
1123
+ elif isinstance(args_delta, dict):
1124
+ # For dict deltas, estimate from JSON representation
1125
+ delta_len = len(json.dumps(args_delta))
1126
+ # Pick a spinner message when tool streaming starts
1127
+ if state.current_spinner_text == "Processing...":
1128
+ import random
1129
+
1130
+ state.current_spinner_text = random.choice( # noqa: S311
1131
+ SPINNER_MESSAGES
1132
+ )
1133
+ elif isinstance(event.delta, TextPartDelta):
1134
+ delta_len = len(event.delta.content_delta)
1135
+
1136
+ if delta_len > 0:
1137
+ # Approximate tokens: len / 4 is a rough estimate
1138
+ state.streamed_tokens += delta_len // 4 + 1
1139
+ # Send progress update for tool call streaming
1140
+ # Throttle updates to every ~75 tokens to avoid flooding UI
1141
+ if is_tool_call_delta and (
1142
+ state.streamed_tokens - state.last_reported_tokens >= 75
1143
+ ):
1144
+ state.last_reported_tokens = state.streamed_tokens
1145
+ self.post_message(
1146
+ ToolStreamingProgressMessage(
1147
+ state.streamed_tokens,
1148
+ state.current_spinner_text,
1149
+ )
1150
+ )
1151
+
981
1152
  try:
982
1153
  updated_part = event.delta.apply(
983
1154
  cast(ModelResponsePart, partial_parts[index])
@@ -1073,6 +1244,17 @@ class AgentManager(Widget):
1073
1244
  if partial_message is not None:
1074
1245
  state.current_response = partial_message
1075
1246
  self._post_partial_message(False)
1247
+
1248
+ # Notify UI that a tool is about to execute
1249
+ # This updates the spinner with a fun message during tool execution
1250
+ # Pick a random spinner message and store it for progress updates
1251
+ import random
1252
+
1253
+ spinner_text = random.choice(SPINNER_MESSAGES) # noqa: S311
1254
+ state.current_spinner_text = spinner_text
1255
+ state.streamed_tokens = 0 # Reset token count for new tool
1256
+ self.post_message(ToolExecutionStartedMessage(spinner_text))
1257
+
1076
1258
  elif isinstance(event, FunctionToolResultEvent):
1077
1259
  # Track tool completion event
1078
1260
 
@@ -1300,7 +1482,7 @@ class AgentManager(Widget):
1300
1482
  Returns:
1301
1483
  ConversationState object containing UI and agent messages and current type
1302
1484
  """
1303
- from shotgun.agents.conversation_history import ConversationState
1485
+ from shotgun.agents.conversation import ConversationState
1304
1486
 
1305
1487
  return ConversationState(
1306
1488
  agent_messages=self.message_history.copy(),