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.
- shotgun/agents/agent_manager.py +497 -30
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +90 -77
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +52 -8
- shotgun/agents/config/models.py +48 -45
- shotgun/agents/config/provider.py +44 -29
- shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/export.py +12 -13
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +90 -2
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +384 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +557 -0
- shotgun/agents/router/tools/plan_tools.py +403 -0
- shotgun/agents/runner.py +17 -2
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +81 -3
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +41 -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 +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/cli/spec/commands.py +2 -0
- shotgun/cli/spec/models.py +18 -0
- shotgun/cli/spec/pull_service.py +122 -68
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +1 -1
- shotgun/main.py +2 -10
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
- shotgun/prompts/agents/plan.j2 +43 -1
- shotgun/prompts/agents/research.j2 +75 -20
- shotgun/prompts/agents/router.j2 +713 -0
- shotgun/prompts/agents/specify.j2 +94 -4
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +24 -15
- shotgun/prompts/agents/tasks.j2 +77 -23
- shotgun/settings.py +44 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
- shotgun/tui/app.py +90 -23
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/prompt_input.py +23 -28
- shotgun/tui/components/status_bar.py +5 -4
- shotgun/tui/dependencies.py +58 -8
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +24 -1
- shotgun/tui/screens/chat/chat_screen.py +1374 -211
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -97
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
- shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +14 -9
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/screens/shotgun_auth.py +50 -0
- shotgun/tui/screens/spec_pull.py +2 -0
- shotgun/tui/state/processing_state.py +19 -0
- 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 +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
- shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/tui/screens/onboarding.py +0 -580
- shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Cancellation utilities for agent execution.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for responsive cancellation of agent operations,
|
|
4
|
+
particularly for handling ESC key presses during LLM streaming.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from collections.abc import AsyncIterable, AsyncIterator
|
|
9
|
+
from typing import TypeVar
|
|
10
|
+
|
|
11
|
+
from shotgun.logging_config import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
CANCELLATION_CHECK_INTERVAL = 0.5 # Check every 500ms
|
|
18
|
+
CANCELLATION_MESSAGE = "Operation cancelled by user"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CancellableStreamIterator(AsyncIterator[T]):
|
|
22
|
+
"""Wraps an async iterable to check for cancellation periodically.
|
|
23
|
+
|
|
24
|
+
This allows ESC cancellation to be responsive even when the underlying
|
|
25
|
+
stream (LLM chunks) is slow to produce events. Instead of blocking
|
|
26
|
+
indefinitely on the next chunk, we timeout periodically and check
|
|
27
|
+
if cancellation was requested.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
cancellation_event = asyncio.Event()
|
|
32
|
+
|
|
33
|
+
async def process_stream(stream):
|
|
34
|
+
wrapped = CancellableStreamIterator(stream, cancellation_event)
|
|
35
|
+
async for event in wrapped:
|
|
36
|
+
process(event)
|
|
37
|
+
|
|
38
|
+
# In another task, set the event to cancel:
|
|
39
|
+
cancellation_event.set()
|
|
40
|
+
```
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
stream: AsyncIterable[T],
|
|
46
|
+
cancellation_event: asyncio.Event | None = None,
|
|
47
|
+
check_interval: float = CANCELLATION_CHECK_INTERVAL,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Initialize the cancellable stream iterator.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
stream: The underlying async iterable to wrap
|
|
53
|
+
cancellation_event: Event that signals cancellation when set
|
|
54
|
+
check_interval: How often to check for cancellation (seconds)
|
|
55
|
+
"""
|
|
56
|
+
self._stream = stream
|
|
57
|
+
self._iterator: AsyncIterator[T] | None = None
|
|
58
|
+
self._cancellation_event = cancellation_event
|
|
59
|
+
self._check_interval = check_interval
|
|
60
|
+
self._pending_task: asyncio.Task[T] | None = None
|
|
61
|
+
|
|
62
|
+
def __aiter__(self) -> AsyncIterator[T]:
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
async def __anext__(self) -> T:
|
|
66
|
+
if self._iterator is None:
|
|
67
|
+
self._iterator = self._stream.__aiter__()
|
|
68
|
+
|
|
69
|
+
# Create a task for the next item if we don't have one pending
|
|
70
|
+
if self._pending_task is None:
|
|
71
|
+
# Capture iterator reference for the coroutine
|
|
72
|
+
iterator = self._iterator
|
|
73
|
+
|
|
74
|
+
async def get_next() -> T:
|
|
75
|
+
return await iterator.__anext__()
|
|
76
|
+
|
|
77
|
+
self._pending_task = asyncio.create_task(get_next())
|
|
78
|
+
|
|
79
|
+
while True:
|
|
80
|
+
# Check if cancellation was requested
|
|
81
|
+
if self._cancellation_event and self._cancellation_event.is_set():
|
|
82
|
+
logger.debug("Cancellation detected in stream iterator")
|
|
83
|
+
# Cancel the pending task and raise
|
|
84
|
+
self._pending_task.cancel()
|
|
85
|
+
self._pending_task = None
|
|
86
|
+
raise asyncio.CancelledError(CANCELLATION_MESSAGE)
|
|
87
|
+
|
|
88
|
+
# Wait for the task with a short timeout
|
|
89
|
+
# Using asyncio.wait instead of wait_for to avoid cancelling the task on timeout
|
|
90
|
+
done, _ = await asyncio.wait(
|
|
91
|
+
[self._pending_task],
|
|
92
|
+
timeout=self._check_interval,
|
|
93
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if done:
|
|
97
|
+
# Task completed - get result and clear pending task
|
|
98
|
+
task = done.pop()
|
|
99
|
+
self._pending_task = None
|
|
100
|
+
# Re-raise StopAsyncIteration or return the result
|
|
101
|
+
return task.result()
|
|
102
|
+
|
|
103
|
+
# Task not done yet, loop and check cancellation again
|
shotgun/agents/common.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Common utilities for agent creation and management."""
|
|
2
2
|
|
|
3
|
-
from collections.abc import Callable
|
|
3
|
+
from collections.abc import AsyncIterable, Awaitable, Callable
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
@@ -17,7 +17,12 @@ from pydantic_ai.messages import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
from shotgun.agents.config import ProviderType, get_provider_model
|
|
20
|
-
from shotgun.agents.models import
|
|
20
|
+
from shotgun.agents.models import (
|
|
21
|
+
AgentResponse,
|
|
22
|
+
AgentSystemPromptContext,
|
|
23
|
+
AgentType,
|
|
24
|
+
ShotgunAgent,
|
|
25
|
+
)
|
|
21
26
|
from shotgun.logging_config import get_logger
|
|
22
27
|
from shotgun.prompts import PromptLoader
|
|
23
28
|
from shotgun.sdk.services import get_codebase_service
|
|
@@ -33,12 +38,14 @@ from .tools import (
|
|
|
33
38
|
codebase_shell,
|
|
34
39
|
directory_lister,
|
|
35
40
|
file_read,
|
|
41
|
+
insert_markdown_section,
|
|
36
42
|
query_graph,
|
|
37
43
|
read_file,
|
|
44
|
+
remove_markdown_section,
|
|
45
|
+
replace_markdown_section,
|
|
38
46
|
retrieve_code,
|
|
39
47
|
write_file,
|
|
40
48
|
)
|
|
41
|
-
from .tools.file_management import AGENT_DIRECTORIES
|
|
42
49
|
|
|
43
50
|
logger = get_logger(__name__)
|
|
44
51
|
|
|
@@ -65,6 +72,11 @@ async def add_system_status_message(
|
|
|
65
72
|
await deps.codebase_service.list_graphs_for_directory()
|
|
66
73
|
)
|
|
67
74
|
|
|
75
|
+
# Get graphs currently being indexed
|
|
76
|
+
indexing_graph_ids: set[str] = set()
|
|
77
|
+
if deps.codebase_service:
|
|
78
|
+
indexing_graph_ids = deps.codebase_service.indexing.get_active_ids()
|
|
79
|
+
|
|
68
80
|
# Get existing files for the agent
|
|
69
81
|
existing_files = get_agent_existing_files(deps.agent_mode)
|
|
70
82
|
|
|
@@ -74,15 +86,31 @@ async def add_system_status_message(
|
|
|
74
86
|
# Get current datetime with timezone information
|
|
75
87
|
dt_context = get_datetime_context()
|
|
76
88
|
|
|
89
|
+
# Get execution plan and pending approval state if this is the Router agent
|
|
90
|
+
execution_plan = None
|
|
91
|
+
pending_approval = False
|
|
92
|
+
if deps.agent_mode == AgentType.ROUTER:
|
|
93
|
+
# Import here to avoid circular imports
|
|
94
|
+
from shotgun.agents.router.models import RouterDeps
|
|
95
|
+
|
|
96
|
+
if isinstance(deps, RouterDeps):
|
|
97
|
+
if deps.current_plan is not None:
|
|
98
|
+
execution_plan = deps.current_plan.format_for_display()
|
|
99
|
+
# Check if plan is pending approval (multi-step plan in Planning mode)
|
|
100
|
+
pending_approval = deps.pending_approval is not None
|
|
101
|
+
|
|
77
102
|
system_state = prompt_loader.render(
|
|
78
103
|
"agents/state/system_state.j2",
|
|
79
104
|
codebase_understanding_graphs=codebase_understanding_graphs,
|
|
105
|
+
indexing_graph_ids=indexing_graph_ids,
|
|
80
106
|
is_tui_context=deps.is_tui_context,
|
|
81
107
|
existing_files=existing_files,
|
|
82
108
|
markdown_toc=markdown_toc,
|
|
83
109
|
current_datetime=dt_context.datetime_formatted,
|
|
84
110
|
timezone_name=dt_context.timezone_name,
|
|
85
111
|
utc_offset=dt_context.utc_offset,
|
|
112
|
+
execution_plan=execution_plan,
|
|
113
|
+
pending_approval=pending_approval,
|
|
86
114
|
)
|
|
87
115
|
|
|
88
116
|
message_history.append(
|
|
@@ -102,7 +130,7 @@ async def create_base_agent(
|
|
|
102
130
|
additional_tools: list[Any] | None = None,
|
|
103
131
|
provider: ProviderType | None = None,
|
|
104
132
|
agent_mode: AgentType | None = None,
|
|
105
|
-
) -> tuple[
|
|
133
|
+
) -> tuple[ShotgunAgent, AgentDeps]:
|
|
106
134
|
"""Create a base agent with common configuration.
|
|
107
135
|
|
|
108
136
|
Args:
|
|
@@ -179,6 +207,9 @@ async def create_base_agent(
|
|
|
179
207
|
agent.tool(write_file)
|
|
180
208
|
agent.tool(append_file)
|
|
181
209
|
agent.tool(read_file)
|
|
210
|
+
agent.tool(replace_markdown_section)
|
|
211
|
+
agent.tool(insert_markdown_section)
|
|
212
|
+
agent.tool(remove_markdown_section)
|
|
182
213
|
|
|
183
214
|
# Register codebase understanding tools (conditional)
|
|
184
215
|
if load_codebase_understanding_tools:
|
|
@@ -352,86 +383,33 @@ async def extract_markdown_toc(agent_mode: AgentType | None) -> str | None:
|
|
|
352
383
|
|
|
353
384
|
|
|
354
385
|
def get_agent_existing_files(agent_mode: AgentType | None = None) -> list[str]:
|
|
355
|
-
"""Get list of existing files
|
|
386
|
+
"""Get list of all existing files in .shotgun directory.
|
|
387
|
+
|
|
388
|
+
All agents can read any file in .shotgun/, so we list all files regardless
|
|
389
|
+
of agent mode. This includes user-added files that agents should be aware of.
|
|
356
390
|
|
|
357
391
|
Args:
|
|
358
|
-
agent_mode:
|
|
392
|
+
agent_mode: Unused, kept for backwards compatibility.
|
|
359
393
|
|
|
360
394
|
Returns:
|
|
361
395
|
List of existing file paths relative to .shotgun directory
|
|
362
396
|
"""
|
|
363
397
|
base_path = get_shotgun_base_path()
|
|
364
|
-
existing_files = []
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if agent_mode is None:
|
|
368
|
-
# List files in the root .shotgun directory
|
|
369
|
-
for item in base_path.iterdir():
|
|
370
|
-
if item.is_file():
|
|
371
|
-
existing_files.append(item.name)
|
|
372
|
-
elif item.is_dir():
|
|
373
|
-
# List files in first-level subdirectories
|
|
374
|
-
for subitem in item.iterdir():
|
|
375
|
-
if subitem.is_file():
|
|
376
|
-
relative_path = subitem.relative_to(base_path)
|
|
377
|
-
existing_files.append(str(relative_path))
|
|
398
|
+
existing_files: list[str] = []
|
|
399
|
+
|
|
400
|
+
if not base_path.exists():
|
|
378
401
|
return existing_files
|
|
379
402
|
|
|
380
|
-
#
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if file_path.is_file():
|
|
390
|
-
relative_path = file_path.relative_to(base_path)
|
|
403
|
+
# List all files in .shotgun directory and subdirectories
|
|
404
|
+
for item in base_path.iterdir():
|
|
405
|
+
if item.is_file():
|
|
406
|
+
existing_files.append(item.name)
|
|
407
|
+
elif item.is_dir():
|
|
408
|
+
# List files in subdirectories (one level deep to avoid too much noise)
|
|
409
|
+
for subitem in item.iterdir():
|
|
410
|
+
if subitem.is_file():
|
|
411
|
+
relative_path = subitem.relative_to(base_path)
|
|
391
412
|
existing_files.append(str(relative_path))
|
|
392
|
-
else:
|
|
393
|
-
# For other agents, check files/directories they have access to
|
|
394
|
-
allowed_paths_raw = AGENT_DIRECTORIES[agent_mode]
|
|
395
|
-
|
|
396
|
-
# Convert single Path/string to list of Paths for uniform handling
|
|
397
|
-
if isinstance(allowed_paths_raw, str):
|
|
398
|
-
# Special case: "*" means export agent (shouldn't reach here but handle it)
|
|
399
|
-
allowed_paths = (
|
|
400
|
-
[Path(allowed_paths_raw)] if allowed_paths_raw != "*" else []
|
|
401
|
-
)
|
|
402
|
-
elif isinstance(allowed_paths_raw, Path):
|
|
403
|
-
allowed_paths = [allowed_paths_raw]
|
|
404
|
-
else:
|
|
405
|
-
# Already a list
|
|
406
|
-
allowed_paths = allowed_paths_raw
|
|
407
|
-
|
|
408
|
-
# Check each allowed path
|
|
409
|
-
for allowed_path in allowed_paths:
|
|
410
|
-
allowed_str = str(allowed_path)
|
|
411
|
-
|
|
412
|
-
# Check if it's a directory (no .md suffix)
|
|
413
|
-
if not allowed_path.suffix or not allowed_str.endswith(".md"):
|
|
414
|
-
# It's a directory - list all files within it
|
|
415
|
-
dir_path = base_path / allowed_str
|
|
416
|
-
if dir_path.exists() and dir_path.is_dir():
|
|
417
|
-
for file_path in dir_path.rglob("*"):
|
|
418
|
-
if file_path.is_file():
|
|
419
|
-
relative_path = file_path.relative_to(base_path)
|
|
420
|
-
existing_files.append(str(relative_path))
|
|
421
|
-
else:
|
|
422
|
-
# It's a file - check if it exists
|
|
423
|
-
file_path = base_path / allowed_str
|
|
424
|
-
if file_path.exists():
|
|
425
|
-
existing_files.append(allowed_str)
|
|
426
|
-
|
|
427
|
-
# Also check for associated directory (e.g., research/ for research.md)
|
|
428
|
-
base_name = allowed_str.replace(".md", "")
|
|
429
|
-
dir_path = base_path / base_name
|
|
430
|
-
if dir_path.exists() and dir_path.is_dir():
|
|
431
|
-
for file_path in dir_path.rglob("*"):
|
|
432
|
-
if file_path.is_file():
|
|
433
|
-
relative_path = file_path.relative_to(base_path)
|
|
434
|
-
existing_files.append(str(relative_path))
|
|
435
413
|
|
|
436
414
|
return existing_files
|
|
437
415
|
|
|
@@ -458,10 +436,24 @@ def build_agent_system_prompt(
|
|
|
458
436
|
logger.debug("🔧 Building research agent system prompt...")
|
|
459
437
|
logger.debug("Interactive mode: %s", ctx.deps.interactive_mode)
|
|
460
438
|
|
|
461
|
-
|
|
462
|
-
|
|
439
|
+
# Build template context using Pydantic model for type safety and testability
|
|
440
|
+
# Import here to avoid circular imports (same pattern as add_system_status_message)
|
|
441
|
+
from shotgun.agents.router.models import RouterDeps
|
|
442
|
+
|
|
443
|
+
router_mode = None
|
|
444
|
+
if isinstance(ctx.deps, RouterDeps):
|
|
445
|
+
router_mode = ctx.deps.router_mode.value
|
|
446
|
+
|
|
447
|
+
template_context = AgentSystemPromptContext(
|
|
463
448
|
interactive_mode=ctx.deps.interactive_mode,
|
|
464
449
|
mode=agent_type,
|
|
450
|
+
sub_agent_context=ctx.deps.sub_agent_context,
|
|
451
|
+
router_mode=router_mode,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
result = prompt_loader.render(
|
|
455
|
+
f"agents/{agent_type}.j2",
|
|
456
|
+
**template_context.model_dump(),
|
|
465
457
|
)
|
|
466
458
|
|
|
467
459
|
if agent_type == "research":
|
|
@@ -525,13 +517,33 @@ async def add_system_prompt_message(
|
|
|
525
517
|
return message_history
|
|
526
518
|
|
|
527
519
|
|
|
520
|
+
EventStreamHandler = Callable[
|
|
521
|
+
[RunContext[AgentDeps], AsyncIterable[Any]], Awaitable[None]
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
|
|
528
525
|
async def run_agent(
|
|
529
|
-
agent:
|
|
526
|
+
agent: ShotgunAgent,
|
|
530
527
|
prompt: str,
|
|
531
528
|
deps: AgentDeps,
|
|
532
529
|
message_history: list[ModelMessage] | None = None,
|
|
533
530
|
usage_limits: UsageLimits | None = None,
|
|
531
|
+
event_stream_handler: EventStreamHandler | None = None,
|
|
534
532
|
) -> AgentRunResult[AgentResponse]:
|
|
533
|
+
"""Run an agent with optional streaming support.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
agent: The agent to run.
|
|
537
|
+
prompt: The prompt to send to the agent.
|
|
538
|
+
deps: Agent dependencies.
|
|
539
|
+
message_history: Optional message history to continue from.
|
|
540
|
+
usage_limits: Optional usage limits for the run.
|
|
541
|
+
event_stream_handler: Optional callback for streaming events.
|
|
542
|
+
When provided, enables real-time streaming of agent responses.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
The agent run result.
|
|
546
|
+
"""
|
|
535
547
|
# Clear file tracker for new run
|
|
536
548
|
deps.file_tracker.clear()
|
|
537
549
|
logger.debug("🔧 Cleared file tracker for new agent run")
|
|
@@ -544,6 +556,7 @@ async def run_agent(
|
|
|
544
556
|
deps=deps,
|
|
545
557
|
usage_limits=usage_limits,
|
|
546
558
|
message_history=message_history,
|
|
559
|
+
event_stream_handler=event_stream_handler,
|
|
547
560
|
)
|
|
548
561
|
|
|
549
562
|
# Log file operations summary if any files were modified
|
shotgun/agents/config/README.md
CHANGED
|
@@ -41,7 +41,6 @@ This directory contains the configuration management system for Shotgun, includi
|
|
|
41
41
|
- **Title**: "feat: add config migration for streaming capability field (v4->v5)"
|
|
42
42
|
- **Key Changes**:
|
|
43
43
|
- Added `supports_streaming` field to OpenAI config
|
|
44
|
-
- Added `shown_onboarding_popup` timestamp field
|
|
45
44
|
- Added `supabase_jwt` to Shotgun Account config
|
|
46
45
|
|
|
47
46
|
## Migration System
|
shotgun/agents/config/manager.py
CHANGED
|
@@ -51,7 +51,7 @@ class ConfigMigrationError(Exception):
|
|
|
51
51
|
ProviderConfig = OpenAIConfig | AnthropicConfig | GoogleConfig | ShotgunAccountConfig
|
|
52
52
|
|
|
53
53
|
# Current config version
|
|
54
|
-
CURRENT_CONFIG_VERSION =
|
|
54
|
+
CURRENT_CONFIG_VERSION = 6
|
|
55
55
|
|
|
56
56
|
# Backup directory name
|
|
57
57
|
BACKUP_DIR_NAME = "backup"
|
|
@@ -183,6 +183,26 @@ def _migrate_v4_to_v5(data: dict[str, Any]) -> dict[str, Any]:
|
|
|
183
183
|
return data
|
|
184
184
|
|
|
185
185
|
|
|
186
|
+
def _migrate_v5_to_v6(data: dict[str, Any]) -> dict[str, Any]:
|
|
187
|
+
"""Migrate config from version 5 to version 6.
|
|
188
|
+
|
|
189
|
+
Changes:
|
|
190
|
+
- Add 'router_mode' field with default 'planning'
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
data: Config data dict at version 5
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Modified config data dict at version 6
|
|
197
|
+
"""
|
|
198
|
+
if "router_mode" not in data:
|
|
199
|
+
data["router_mode"] = "planning"
|
|
200
|
+
logger.info("Migrated config v5->v6: added router_mode field")
|
|
201
|
+
|
|
202
|
+
data["config_version"] = 6
|
|
203
|
+
return data
|
|
204
|
+
|
|
205
|
+
|
|
186
206
|
def _apply_migrations(data: dict[str, Any]) -> dict[str, Any]:
|
|
187
207
|
"""Apply all necessary migrations to bring config to current version.
|
|
188
208
|
|
|
@@ -203,6 +223,7 @@ def _apply_migrations(data: dict[str, Any]) -> dict[str, Any]:
|
|
|
203
223
|
2: _migrate_v2_to_v3,
|
|
204
224
|
3: _migrate_v3_to_v4,
|
|
205
225
|
4: _migrate_v4_to_v5,
|
|
226
|
+
5: _migrate_v5_to_v6,
|
|
206
227
|
}
|
|
207
228
|
|
|
208
229
|
# Apply migrations sequentially
|
|
@@ -356,9 +377,9 @@ class ConfigManager:
|
|
|
356
377
|
|
|
357
378
|
# Find default model for this provider
|
|
358
379
|
provider_models = {
|
|
359
|
-
ProviderType.OPENAI: ModelName.
|
|
360
|
-
ProviderType.ANTHROPIC: ModelName.
|
|
361
|
-
ProviderType.GOOGLE: ModelName.
|
|
380
|
+
ProviderType.OPENAI: ModelName.GPT_5_2,
|
|
381
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
|
|
382
|
+
ProviderType.GOOGLE: ModelName.GEMINI_3_PRO_PREVIEW,
|
|
362
383
|
}
|
|
363
384
|
|
|
364
385
|
if provider in provider_models:
|
|
@@ -500,15 +521,18 @@ class ConfigManager:
|
|
|
500
521
|
if provider_enum is None:
|
|
501
522
|
raise RuntimeError("Provider enum should not be None for LLM providers")
|
|
502
523
|
other_providers = [p for p in ProviderType if p != provider_enum]
|
|
503
|
-
has_other_keys = any(
|
|
524
|
+
has_other_keys = any(
|
|
525
|
+
self._provider_has_api_key(self._get_provider_config(config, p))
|
|
526
|
+
for p in other_providers
|
|
527
|
+
)
|
|
504
528
|
if not has_other_keys:
|
|
505
529
|
# Set selected_model to this provider's default model
|
|
506
530
|
from .models import ModelName
|
|
507
531
|
|
|
508
532
|
provider_models = {
|
|
509
|
-
ProviderType.OPENAI: ModelName.
|
|
510
|
-
ProviderType.ANTHROPIC: ModelName.
|
|
511
|
-
ProviderType.GOOGLE: ModelName.
|
|
533
|
+
ProviderType.OPENAI: ModelName.GPT_5_2,
|
|
534
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
|
|
535
|
+
ProviderType.GOOGLE: ModelName.GEMINI_3_PRO_PREVIEW,
|
|
512
536
|
}
|
|
513
537
|
if provider_enum in provider_models:
|
|
514
538
|
config.selected_model = provider_models[provider_enum]
|
|
@@ -772,6 +796,26 @@ class ConfigManager:
|
|
|
772
796
|
await self.save(config)
|
|
773
797
|
logger.info("Updated Shotgun Account configuration")
|
|
774
798
|
|
|
799
|
+
async def get_router_mode(self) -> str:
|
|
800
|
+
"""Get the saved router mode.
|
|
801
|
+
|
|
802
|
+
Returns:
|
|
803
|
+
The router mode string ('planning' or 'drafting')
|
|
804
|
+
"""
|
|
805
|
+
config = await self.load()
|
|
806
|
+
return config.router_mode
|
|
807
|
+
|
|
808
|
+
async def set_router_mode(self, mode: str) -> None:
|
|
809
|
+
"""Save the router mode.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
mode: Router mode to save ('planning' or 'drafting')
|
|
813
|
+
"""
|
|
814
|
+
config = await self.load()
|
|
815
|
+
config.router_mode = mode
|
|
816
|
+
await self.save(config)
|
|
817
|
+
logger.debug("Router mode saved: %s", mode)
|
|
818
|
+
|
|
775
819
|
|
|
776
820
|
# Global singleton instance
|
|
777
821
|
_config_manager_instance: ConfigManager | None = None
|
shotgun/agents/config/models.py
CHANGED
|
@@ -25,16 +25,15 @@ class KeyProvider(StrEnum):
|
|
|
25
25
|
class ModelName(StrEnum):
|
|
26
26
|
"""Available AI model names."""
|
|
27
27
|
|
|
28
|
-
GPT_5 = "gpt-5"
|
|
29
|
-
GPT_5_MINI = "gpt-5-mini"
|
|
30
28
|
GPT_5_1 = "gpt-5.1"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
CLAUDE_OPUS_4_1 = "claude-opus-4-1"
|
|
29
|
+
GPT_5_2 = "gpt-5.2"
|
|
30
|
+
CLAUDE_OPUS_4_5 = "claude-opus-4-5"
|
|
34
31
|
CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
|
|
35
32
|
CLAUDE_HAIKU_4_5 = "claude-haiku-4-5"
|
|
36
33
|
GEMINI_2_5_PRO = "gemini-2.5-pro"
|
|
37
34
|
GEMINI_2_5_FLASH = "gemini-2.5-flash"
|
|
35
|
+
GEMINI_2_5_FLASH_LITE = "gemini-2.5-flash-lite"
|
|
36
|
+
GEMINI_3_PRO_PREVIEW = "gemini-3-pro-preview"
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
class ModelSpec(BaseModel):
|
|
@@ -101,22 +100,6 @@ class ModelConfig(BaseModel):
|
|
|
101
100
|
|
|
102
101
|
# Model specifications registry (static metadata)
|
|
103
102
|
MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
104
|
-
ModelName.GPT_5: ModelSpec(
|
|
105
|
-
name=ModelName.GPT_5,
|
|
106
|
-
provider=ProviderType.OPENAI,
|
|
107
|
-
max_input_tokens=400_000,
|
|
108
|
-
max_output_tokens=128_000,
|
|
109
|
-
litellm_proxy_model_name="openai/gpt-5",
|
|
110
|
-
short_name="GPT-5",
|
|
111
|
-
),
|
|
112
|
-
ModelName.GPT_5_MINI: ModelSpec(
|
|
113
|
-
name=ModelName.GPT_5_MINI,
|
|
114
|
-
provider=ProviderType.OPENAI,
|
|
115
|
-
max_input_tokens=400_000,
|
|
116
|
-
max_output_tokens=128_000,
|
|
117
|
-
litellm_proxy_model_name="openai/gpt-5-mini",
|
|
118
|
-
short_name="GPT-5 Mini",
|
|
119
|
-
),
|
|
120
103
|
ModelName.GPT_5_1: ModelSpec(
|
|
121
104
|
name=ModelName.GPT_5_1,
|
|
122
105
|
provider=ProviderType.OPENAI,
|
|
@@ -125,29 +108,13 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
125
108
|
litellm_proxy_model_name="openai/gpt-5.1",
|
|
126
109
|
short_name="GPT-5.1",
|
|
127
110
|
),
|
|
128
|
-
ModelName.
|
|
129
|
-
name=ModelName.
|
|
111
|
+
ModelName.GPT_5_2: ModelSpec(
|
|
112
|
+
name=ModelName.GPT_5_2,
|
|
130
113
|
provider=ProviderType.OPENAI,
|
|
131
114
|
max_input_tokens=272_000,
|
|
132
115
|
max_output_tokens=128_000,
|
|
133
|
-
litellm_proxy_model_name="openai/gpt-5.
|
|
134
|
-
short_name="GPT-5.
|
|
135
|
-
),
|
|
136
|
-
ModelName.GPT_5_1_CODEX_MINI: ModelSpec(
|
|
137
|
-
name=ModelName.GPT_5_1_CODEX_MINI,
|
|
138
|
-
provider=ProviderType.OPENAI,
|
|
139
|
-
max_input_tokens=272_000,
|
|
140
|
-
max_output_tokens=128_000,
|
|
141
|
-
litellm_proxy_model_name="openai/gpt-5.1-codex-mini",
|
|
142
|
-
short_name="GPT-5.1 Codex Mini",
|
|
143
|
-
),
|
|
144
|
-
ModelName.CLAUDE_OPUS_4_1: ModelSpec(
|
|
145
|
-
name=ModelName.CLAUDE_OPUS_4_1,
|
|
146
|
-
provider=ProviderType.ANTHROPIC,
|
|
147
|
-
max_input_tokens=200_000,
|
|
148
|
-
max_output_tokens=32_000,
|
|
149
|
-
litellm_proxy_model_name="anthropic/claude-opus-4-1",
|
|
150
|
-
short_name="Opus 4.1",
|
|
116
|
+
litellm_proxy_model_name="openai/gpt-5.2",
|
|
117
|
+
short_name="GPT-5.2",
|
|
151
118
|
),
|
|
152
119
|
ModelName.CLAUDE_SONNET_4_5: ModelSpec(
|
|
153
120
|
name=ModelName.CLAUDE_SONNET_4_5,
|
|
@@ -181,6 +148,30 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
181
148
|
litellm_proxy_model_name="gemini/gemini-2.5-flash",
|
|
182
149
|
short_name="Gemini 2.5 Flash",
|
|
183
150
|
),
|
|
151
|
+
ModelName.CLAUDE_OPUS_4_5: ModelSpec(
|
|
152
|
+
name=ModelName.CLAUDE_OPUS_4_5,
|
|
153
|
+
provider=ProviderType.ANTHROPIC,
|
|
154
|
+
max_input_tokens=200_000,
|
|
155
|
+
max_output_tokens=64_000,
|
|
156
|
+
litellm_proxy_model_name="anthropic/claude-opus-4-5",
|
|
157
|
+
short_name="Opus 4.5",
|
|
158
|
+
),
|
|
159
|
+
ModelName.GEMINI_2_5_FLASH_LITE: ModelSpec(
|
|
160
|
+
name=ModelName.GEMINI_2_5_FLASH_LITE,
|
|
161
|
+
provider=ProviderType.GOOGLE,
|
|
162
|
+
max_input_tokens=1_048_576,
|
|
163
|
+
max_output_tokens=65_536,
|
|
164
|
+
litellm_proxy_model_name="gemini/gemini-2.5-flash-lite",
|
|
165
|
+
short_name="Gemini 2.5 Flash Lite",
|
|
166
|
+
),
|
|
167
|
+
ModelName.GEMINI_3_PRO_PREVIEW: ModelSpec(
|
|
168
|
+
name=ModelName.GEMINI_3_PRO_PREVIEW,
|
|
169
|
+
provider=ProviderType.GOOGLE,
|
|
170
|
+
max_input_tokens=1_048_576,
|
|
171
|
+
max_output_tokens=65_536,
|
|
172
|
+
litellm_proxy_model_name="gemini/gemini-3-pro-preview",
|
|
173
|
+
short_name="Gemini 3 Pro",
|
|
174
|
+
),
|
|
184
175
|
}
|
|
185
176
|
|
|
186
177
|
|
|
@@ -217,6 +208,18 @@ class ShotgunAccountConfig(BaseModel):
|
|
|
217
208
|
default=None, description="Default workspace ID for shared specs"
|
|
218
209
|
)
|
|
219
210
|
|
|
211
|
+
@property
|
|
212
|
+
def has_valid_account(self) -> bool:
|
|
213
|
+
"""Check if the user has a valid Shotgun Account configured.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
True if api_key is set and non-empty, False otherwise
|
|
217
|
+
"""
|
|
218
|
+
if self.api_key is None:
|
|
219
|
+
return False
|
|
220
|
+
value = self.api_key.get_secret_value()
|
|
221
|
+
return bool(value and value.strip())
|
|
222
|
+
|
|
220
223
|
|
|
221
224
|
class MarketingMessageRecord(BaseModel):
|
|
222
225
|
"""Record of when a marketing message was shown to the user."""
|
|
@@ -252,10 +255,6 @@ class ShotgunConfig(BaseModel):
|
|
|
252
255
|
default=False,
|
|
253
256
|
description="Whether the welcome screen has been shown to the user",
|
|
254
257
|
)
|
|
255
|
-
shown_onboarding_popup: datetime | None = Field(
|
|
256
|
-
default=None,
|
|
257
|
-
description="Timestamp when the onboarding popup was shown to the user (ISO8601 format)",
|
|
258
|
-
)
|
|
259
258
|
marketing: MarketingConfig = Field(
|
|
260
259
|
default_factory=MarketingConfig,
|
|
261
260
|
description="Marketing messages configuration and tracking",
|
|
@@ -268,3 +267,7 @@ class ShotgunConfig(BaseModel):
|
|
|
268
267
|
default=None,
|
|
269
268
|
description="Path to the backup file created when migration failed",
|
|
270
269
|
)
|
|
270
|
+
router_mode: str = Field(
|
|
271
|
+
default="planning",
|
|
272
|
+
description="Router execution mode: 'planning' or 'drafting'",
|
|
273
|
+
)
|