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
shotgun/agents/agent_manager.py
CHANGED
|
@@ -17,10 +17,9 @@ from tenacity import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
|
-
from shotgun.agents.
|
|
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:
|
|
319
|
+
self._research_agent: ShotgunAgent | None = None
|
|
249
320
|
self._research_deps: AgentDeps | None = None
|
|
250
|
-
self._plan_agent:
|
|
321
|
+
self._plan_agent: ShotgunAgent | None = None
|
|
251
322
|
self._plan_deps: AgentDeps | None = None
|
|
252
|
-
self._tasks_agent:
|
|
323
|
+
self._tasks_agent: ShotgunAgent | None = None
|
|
253
324
|
self._tasks_deps: AgentDeps | None = None
|
|
254
|
-
self._specify_agent:
|
|
325
|
+
self._specify_agent: ShotgunAgent | None = None
|
|
255
326
|
self._specify_deps: AgentDeps | None = None
|
|
256
|
-
self._export_agent:
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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
|
-
#
|
|
449
|
-
#
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
|
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.
|
|
1485
|
+
from shotgun.agents.conversation import ConversationState
|
|
1304
1486
|
|
|
1305
1487
|
return ConversationState(
|
|
1306
1488
|
agent_messages=self.message_history.copy(),
|