shotgun-sh 0.2.3.dev2__py3-none-any.whl → 0.2.11.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 +524 -58
- shotgun/agents/common.py +62 -62
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +14 -3
- shotgun/agents/config/models.py +16 -0
- shotgun/agents/config/provider.py +68 -13
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +493 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation_history.py +125 -2
- shotgun/agents/conversation_manager.py +24 -2
- shotgun/agents/export.py +4 -5
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +14 -2
- shotgun/agents/history/token_counting/anthropic.py +32 -10
- shotgun/agents/models.py +50 -2
- shotgun/agents/plan.py +4 -5
- shotgun/agents/research.py +4 -5
- shotgun/agents/specify.py +4 -5
- shotgun/agents/tasks.py +4 -5
- shotgun/agents/tools/__init__.py +0 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +6 -0
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +71 -9
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +24 -12
- shotgun/agents/tools/web_search/anthropic.py +24 -3
- shotgun/agents/tools/web_search/gemini.py +22 -10
- shotgun/agents/tools/web_search/openai.py +21 -12
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +1 -1
- shotgun/cli/clear.py +52 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/context.py +111 -0
- shotgun/cli/models.py +1 -0
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/manager.py +10 -1
- shotgun/llm_proxy/__init__.py +5 -2
- shotgun/llm_proxy/clients.py +12 -7
- shotgun/logging_config.py +8 -10
- shotgun/main.py +70 -10
- shotgun/posthog_telemetry.py +9 -3
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/state/system_state.j2 +4 -0
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/prompts/loader.py +2 -2
- shotgun/prompts/tools/web_search.j2 +14 -0
- shotgun/sentry_telemetry.py +4 -15
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +15 -32
- shotgun/tui/app.py +203 -9
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +136 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +93 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1110 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +39 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +68 -2
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +151 -0
- shotgun/tui/screens/model_picker.py +30 -6
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/welcome.py +24 -5
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +182 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +247 -0
- shotgun/utils/datetime_utils.py +77 -0
- shotgun/utils/file_system_utils.py +3 -2
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.11.dev1.dist-info/METADATA +129 -0
- shotgun_sh-0.2.11.dev1.dist-info/RECORD +190 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -804
- shotgun/tui/screens/chat_screen/history.py +0 -352
- shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
- shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/WHEEL +0 -0
|
@@ -127,6 +127,7 @@ calculate_max_summarization_tokens = _calculate_max_summarization_tokens
|
|
|
127
127
|
async def token_limit_compactor(
|
|
128
128
|
ctx: ContextProtocol,
|
|
129
129
|
messages: list[ModelMessage],
|
|
130
|
+
force: bool = False,
|
|
130
131
|
) -> list[ModelMessage]:
|
|
131
132
|
"""Compact message history based on token limits with incremental processing.
|
|
132
133
|
|
|
@@ -139,6 +140,7 @@ async def token_limit_compactor(
|
|
|
139
140
|
Args:
|
|
140
141
|
ctx: Run context with usage information and dependencies
|
|
141
142
|
messages: Current conversation history
|
|
143
|
+
force: If True, force compaction even if below token threshold
|
|
142
144
|
|
|
143
145
|
Returns:
|
|
144
146
|
Compacted list of messages within token limits
|
|
@@ -169,7 +171,7 @@ async def token_limit_compactor(
|
|
|
169
171
|
)
|
|
170
172
|
|
|
171
173
|
# Only do incremental compaction if post-summary conversation exceeds threshold
|
|
172
|
-
if post_summary_tokens < max_tokens:
|
|
174
|
+
if post_summary_tokens < max_tokens and not force:
|
|
173
175
|
logger.debug(
|
|
174
176
|
f"Post-summary conversation under threshold ({post_summary_tokens} < {max_tokens}), "
|
|
175
177
|
f"keeping all {len(messages)} messages"
|
|
@@ -340,6 +342,7 @@ async def token_limit_compactor(
|
|
|
340
342
|
else 0
|
|
341
343
|
)
|
|
342
344
|
|
|
345
|
+
# Track incremental compaction with simple metrics (fast, no token counting)
|
|
343
346
|
track_event(
|
|
344
347
|
"context_compaction_triggered",
|
|
345
348
|
{
|
|
@@ -352,6 +355,10 @@ async def token_limit_compactor(
|
|
|
352
355
|
"agent_mode": deps.agent_mode.value
|
|
353
356
|
if hasattr(deps, "agent_mode") and deps.agent_mode
|
|
354
357
|
else "unknown",
|
|
358
|
+
# Model and provider info (no computation needed)
|
|
359
|
+
"model_name": deps.llm_model.name.value,
|
|
360
|
+
"provider": deps.llm_model.provider.value,
|
|
361
|
+
"key_provider": deps.llm_model.key_provider.value,
|
|
355
362
|
},
|
|
356
363
|
)
|
|
357
364
|
|
|
@@ -368,7 +375,7 @@ async def token_limit_compactor(
|
|
|
368
375
|
)
|
|
369
376
|
|
|
370
377
|
# Only do full compaction if total conversation exceeds threshold
|
|
371
|
-
if total_tokens < max_tokens:
|
|
378
|
+
if total_tokens < max_tokens and not force:
|
|
372
379
|
logger.debug(
|
|
373
380
|
f"Total conversation under threshold ({total_tokens} < {max_tokens}), "
|
|
374
381
|
f"keeping all {len(messages)} messages"
|
|
@@ -468,6 +475,7 @@ async def _full_compaction(
|
|
|
468
475
|
tokens_before = current_tokens # Already calculated above
|
|
469
476
|
tokens_after = summary_usage.output_tokens if summary_usage else 0
|
|
470
477
|
|
|
478
|
+
# Track full compaction with simple metrics (fast, no token counting)
|
|
471
479
|
track_event(
|
|
472
480
|
"context_compaction_triggered",
|
|
473
481
|
{
|
|
@@ -480,6 +488,10 @@ async def _full_compaction(
|
|
|
480
488
|
"agent_mode": deps.agent_mode.value
|
|
481
489
|
if hasattr(deps, "agent_mode") and deps.agent_mode
|
|
482
490
|
else "unknown",
|
|
491
|
+
# Model and provider info (no computation needed)
|
|
492
|
+
"model_name": deps.llm_model.name.value,
|
|
493
|
+
"provider": deps.llm_model.provider.value,
|
|
494
|
+
"key_provider": deps.llm_model.key_provider.value,
|
|
483
495
|
},
|
|
484
496
|
)
|
|
485
497
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Anthropic token counting using official client."""
|
|
2
2
|
|
|
3
|
+
import logfire
|
|
3
4
|
from pydantic_ai.messages import ModelMessage
|
|
4
5
|
|
|
5
6
|
from shotgun.agents.config.models import KeyProvider
|
|
6
|
-
from shotgun.llm_proxy import
|
|
7
|
+
from shotgun.llm_proxy import create_anthropic_proxy_provider
|
|
7
8
|
from shotgun.logging_config import get_logger
|
|
8
9
|
|
|
9
10
|
from .base import TokenCounter, extract_text_from_messages
|
|
@@ -36,19 +37,28 @@ class AnthropicTokenCounter(TokenCounter):
|
|
|
36
37
|
try:
|
|
37
38
|
if key_provider == KeyProvider.SHOTGUN:
|
|
38
39
|
# Use LiteLLM proxy for Shotgun Account
|
|
39
|
-
#
|
|
40
|
-
|
|
40
|
+
# Get async client from AnthropicProvider
|
|
41
|
+
provider = create_anthropic_proxy_provider(api_key)
|
|
42
|
+
self.client = provider.client
|
|
41
43
|
logger.debug(
|
|
42
|
-
f"Initialized Anthropic token counter for {model_name} via LiteLLM proxy"
|
|
44
|
+
f"Initialized async Anthropic token counter for {model_name} via LiteLLM proxy"
|
|
43
45
|
)
|
|
44
46
|
else:
|
|
45
|
-
# Direct Anthropic API for BYOK
|
|
46
|
-
self.client = anthropic.
|
|
47
|
+
# Direct Anthropic API for BYOK - use async client
|
|
48
|
+
self.client = anthropic.AsyncAnthropic(api_key=api_key)
|
|
47
49
|
logger.debug(
|
|
48
|
-
f"Initialized Anthropic token counter for {model_name} via direct API"
|
|
50
|
+
f"Initialized async Anthropic token counter for {model_name} via direct API"
|
|
49
51
|
)
|
|
50
52
|
except Exception as e:
|
|
51
|
-
|
|
53
|
+
logfire.exception(
|
|
54
|
+
f"Failed to initialize Anthropic token counter for {model_name}",
|
|
55
|
+
model_name=model_name,
|
|
56
|
+
key_provider=key_provider.value,
|
|
57
|
+
exception_type=type(e).__name__,
|
|
58
|
+
)
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"Failed to initialize Anthropic async client for {model_name}: {type(e).__name__}: {str(e)}"
|
|
61
|
+
) from e
|
|
52
62
|
|
|
53
63
|
async def count_tokens(self, text: str) -> int:
|
|
54
64
|
"""Count tokens using Anthropic's official API (async).
|
|
@@ -64,13 +74,25 @@ class AnthropicTokenCounter(TokenCounter):
|
|
|
64
74
|
"""
|
|
65
75
|
try:
|
|
66
76
|
# Anthropic API expects messages format and model parameter
|
|
67
|
-
|
|
77
|
+
# Use await with async client
|
|
78
|
+
result = await self.client.messages.count_tokens(
|
|
68
79
|
messages=[{"role": "user", "content": text}], model=self.model_name
|
|
69
80
|
)
|
|
70
81
|
return result.input_tokens
|
|
71
82
|
except Exception as e:
|
|
83
|
+
# Create a preview of the text for logging (truncated to avoid huge logs)
|
|
84
|
+
text_preview = text[:100] + "..." if len(text) > 100 else text
|
|
85
|
+
|
|
86
|
+
logfire.exception(
|
|
87
|
+
f"Anthropic token counting failed for {self.model_name}",
|
|
88
|
+
model_name=self.model_name,
|
|
89
|
+
text_length=len(text),
|
|
90
|
+
text_preview=text_preview,
|
|
91
|
+
exception_type=type(e).__name__,
|
|
92
|
+
exception_message=str(e),
|
|
93
|
+
)
|
|
72
94
|
raise RuntimeError(
|
|
73
|
-
f"Anthropic token counting API failed for {self.model_name}"
|
|
95
|
+
f"Anthropic token counting API failed for {self.model_name}: {type(e).__name__}: {str(e)}"
|
|
74
96
|
) from e
|
|
75
97
|
|
|
76
98
|
async def count_message_tokens(self, messages: list[ModelMessage]) -> int:
|
shotgun/agents/models.py
CHANGED
|
@@ -19,6 +19,30 @@ if TYPE_CHECKING:
|
|
|
19
19
|
from shotgun.codebase.service import CodebaseService
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class AgentResponse(BaseModel):
|
|
23
|
+
"""Structured response from an agent with optional clarifying questions.
|
|
24
|
+
|
|
25
|
+
This model provides a consistent response format for all agents:
|
|
26
|
+
- response: The main response text (can be empty if only asking questions)
|
|
27
|
+
- clarifying_questions: Optional list of questions to ask the user
|
|
28
|
+
|
|
29
|
+
When clarifying_questions is provided, the agent expects to receive
|
|
30
|
+
answers before continuing its work. This replaces the ask_questions tool.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
response: str = Field(
|
|
34
|
+
description="The agent's response text. Always respond with some text summarizing what happened, whats next, etc.",
|
|
35
|
+
)
|
|
36
|
+
clarifying_questions: list[str] | None = Field(
|
|
37
|
+
default=None,
|
|
38
|
+
description="""
|
|
39
|
+
Optional list of clarifying questions to ask the user.
|
|
40
|
+
- Single question: Shown as a non-blocking suggestion (user can answer or continue with other prompts)
|
|
41
|
+
- Multiple questions (2+): Asked sequentially in Q&A mode (blocks input until all answered or cancelled)
|
|
42
|
+
""",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
22
46
|
class AgentType(StrEnum):
|
|
23
47
|
"""Enumeration for available agent types."""
|
|
24
48
|
|
|
@@ -73,6 +97,30 @@ class UserQuestion(BaseModel):
|
|
|
73
97
|
)
|
|
74
98
|
|
|
75
99
|
|
|
100
|
+
class MultipleUserQuestions(BaseModel):
|
|
101
|
+
"""Multiple questions to ask the user sequentially."""
|
|
102
|
+
|
|
103
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
104
|
+
|
|
105
|
+
questions: list[str] = Field(
|
|
106
|
+
description="List of questions to ask the user",
|
|
107
|
+
)
|
|
108
|
+
current_index: int = Field(
|
|
109
|
+
default=0,
|
|
110
|
+
description="Current question index being asked",
|
|
111
|
+
)
|
|
112
|
+
answers: list[str] = Field(
|
|
113
|
+
default_factory=list,
|
|
114
|
+
description="Accumulated answers from the user",
|
|
115
|
+
)
|
|
116
|
+
tool_call_id: str = Field(
|
|
117
|
+
description="Tool call id",
|
|
118
|
+
)
|
|
119
|
+
result: Future[UserAnswer] = Field(
|
|
120
|
+
description="Future that will contain all answers formatted as Q&A pairs"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
76
124
|
class AgentRuntimeOptions(BaseModel):
|
|
77
125
|
"""User interface options for agents."""
|
|
78
126
|
|
|
@@ -100,9 +148,9 @@ class AgentRuntimeOptions(BaseModel):
|
|
|
100
148
|
description="Maximum number of iterations for agent loops",
|
|
101
149
|
)
|
|
102
150
|
|
|
103
|
-
queue: Queue[UserQuestion] = Field(
|
|
151
|
+
queue: Queue[UserQuestion | MultipleUserQuestions] = Field(
|
|
104
152
|
default_factory=Queue,
|
|
105
|
-
description="Queue for storing user
|
|
153
|
+
description="Queue for storing user questions (single or multiple)",
|
|
106
154
|
)
|
|
107
155
|
|
|
108
156
|
tasks: list[Future[UserAnswer]] = Field(
|
shotgun/agents/plan.py
CHANGED
|
@@ -4,7 +4,6 @@ from functools import partial
|
|
|
4
4
|
|
|
5
5
|
from pydantic_ai import (
|
|
6
6
|
Agent,
|
|
7
|
-
DeferredToolRequests,
|
|
8
7
|
)
|
|
9
8
|
from pydantic_ai.agent import AgentRunResult
|
|
10
9
|
from pydantic_ai.messages import ModelMessage
|
|
@@ -19,14 +18,14 @@ from .common import (
|
|
|
19
18
|
create_usage_limits,
|
|
20
19
|
run_agent,
|
|
21
20
|
)
|
|
22
|
-
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
21
|
+
from .models import AgentDeps, AgentResponse, AgentRuntimeOptions, AgentType
|
|
23
22
|
|
|
24
23
|
logger = get_logger(__name__)
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def create_plan_agent(
|
|
28
27
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
29
|
-
) -> tuple[Agent[AgentDeps,
|
|
28
|
+
) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
|
|
30
29
|
"""Create a plan agent with artifact management capabilities.
|
|
31
30
|
|
|
32
31
|
Args:
|
|
@@ -52,11 +51,11 @@ def create_plan_agent(
|
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
async def run_plan_agent(
|
|
55
|
-
agent: Agent[AgentDeps,
|
|
54
|
+
agent: Agent[AgentDeps, AgentResponse],
|
|
56
55
|
goal: str,
|
|
57
56
|
deps: AgentDeps,
|
|
58
57
|
message_history: list[ModelMessage] | None = None,
|
|
59
|
-
) -> AgentRunResult[
|
|
58
|
+
) -> AgentRunResult[AgentResponse]:
|
|
60
59
|
"""Create or update a plan based on the given goal using artifacts.
|
|
61
60
|
|
|
62
61
|
Args:
|
shotgun/agents/research.py
CHANGED
|
@@ -4,7 +4,6 @@ from functools import partial
|
|
|
4
4
|
|
|
5
5
|
from pydantic_ai import (
|
|
6
6
|
Agent,
|
|
7
|
-
DeferredToolRequests,
|
|
8
7
|
)
|
|
9
8
|
from pydantic_ai.agent import AgentRunResult
|
|
10
9
|
from pydantic_ai.messages import (
|
|
@@ -21,7 +20,7 @@ from .common import (
|
|
|
21
20
|
create_usage_limits,
|
|
22
21
|
run_agent,
|
|
23
22
|
)
|
|
24
|
-
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
23
|
+
from .models import AgentDeps, AgentResponse, AgentRuntimeOptions, AgentType
|
|
25
24
|
from .tools import get_available_web_search_tools
|
|
26
25
|
|
|
27
26
|
logger = get_logger(__name__)
|
|
@@ -29,7 +28,7 @@ logger = get_logger(__name__)
|
|
|
29
28
|
|
|
30
29
|
def create_research_agent(
|
|
31
30
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
32
|
-
) -> tuple[Agent[AgentDeps,
|
|
31
|
+
) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
|
|
33
32
|
"""Create a research agent with web search and artifact management capabilities.
|
|
34
33
|
|
|
35
34
|
Args:
|
|
@@ -66,11 +65,11 @@ def create_research_agent(
|
|
|
66
65
|
|
|
67
66
|
|
|
68
67
|
async def run_research_agent(
|
|
69
|
-
agent: Agent[AgentDeps,
|
|
68
|
+
agent: Agent[AgentDeps, AgentResponse],
|
|
70
69
|
query: str,
|
|
71
70
|
deps: AgentDeps,
|
|
72
71
|
message_history: list[ModelMessage] | None = None,
|
|
73
|
-
) -> AgentRunResult[
|
|
72
|
+
) -> AgentRunResult[AgentResponse]:
|
|
74
73
|
"""Perform research on the given query and update research artifacts.
|
|
75
74
|
|
|
76
75
|
Args:
|
shotgun/agents/specify.py
CHANGED
|
@@ -4,7 +4,6 @@ from functools import partial
|
|
|
4
4
|
|
|
5
5
|
from pydantic_ai import (
|
|
6
6
|
Agent,
|
|
7
|
-
DeferredToolRequests,
|
|
8
7
|
)
|
|
9
8
|
from pydantic_ai.agent import AgentRunResult
|
|
10
9
|
from pydantic_ai.messages import ModelMessage
|
|
@@ -19,14 +18,14 @@ from .common import (
|
|
|
19
18
|
create_usage_limits,
|
|
20
19
|
run_agent,
|
|
21
20
|
)
|
|
22
|
-
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
21
|
+
from .models import AgentDeps, AgentResponse, AgentRuntimeOptions, AgentType
|
|
23
22
|
|
|
24
23
|
logger = get_logger(__name__)
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def create_specify_agent(
|
|
28
27
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
29
|
-
) -> tuple[Agent[AgentDeps,
|
|
28
|
+
) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
|
|
30
29
|
"""Create a specify agent with artifact management capabilities.
|
|
31
30
|
|
|
32
31
|
Args:
|
|
@@ -52,11 +51,11 @@ def create_specify_agent(
|
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
async def run_specify_agent(
|
|
55
|
-
agent: Agent[AgentDeps,
|
|
54
|
+
agent: Agent[AgentDeps, AgentResponse],
|
|
56
55
|
requirement: str,
|
|
57
56
|
deps: AgentDeps,
|
|
58
57
|
message_history: list[ModelMessage] | None = None,
|
|
59
|
-
) -> AgentRunResult[
|
|
58
|
+
) -> AgentRunResult[AgentResponse]:
|
|
60
59
|
"""Create or update specifications based on the given requirement.
|
|
61
60
|
|
|
62
61
|
Args:
|
shotgun/agents/tasks.py
CHANGED
|
@@ -4,7 +4,6 @@ from functools import partial
|
|
|
4
4
|
|
|
5
5
|
from pydantic_ai import (
|
|
6
6
|
Agent,
|
|
7
|
-
DeferredToolRequests,
|
|
8
7
|
)
|
|
9
8
|
from pydantic_ai.agent import AgentRunResult
|
|
10
9
|
from pydantic_ai.messages import ModelMessage
|
|
@@ -19,14 +18,14 @@ from .common import (
|
|
|
19
18
|
create_usage_limits,
|
|
20
19
|
run_agent,
|
|
21
20
|
)
|
|
22
|
-
from .models import AgentDeps, AgentRuntimeOptions, AgentType
|
|
21
|
+
from .models import AgentDeps, AgentResponse, AgentRuntimeOptions, AgentType
|
|
23
22
|
|
|
24
23
|
logger = get_logger(__name__)
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def create_tasks_agent(
|
|
28
27
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
29
|
-
) -> tuple[Agent[AgentDeps,
|
|
28
|
+
) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
|
|
30
29
|
"""Create a tasks agent with file management capabilities.
|
|
31
30
|
|
|
32
31
|
Args:
|
|
@@ -50,11 +49,11 @@ def create_tasks_agent(
|
|
|
50
49
|
|
|
51
50
|
|
|
52
51
|
async def run_tasks_agent(
|
|
53
|
-
agent: Agent[AgentDeps,
|
|
52
|
+
agent: Agent[AgentDeps, AgentResponse],
|
|
54
53
|
instruction: str,
|
|
55
54
|
deps: AgentDeps,
|
|
56
55
|
message_history: list[ModelMessage] | None = None,
|
|
57
|
-
) -> AgentRunResult[
|
|
56
|
+
) -> AgentRunResult[AgentResponse]:
|
|
58
57
|
"""Create or update tasks based on the given instruction.
|
|
59
58
|
|
|
60
59
|
Args:
|
shotgun/agents/tools/__init__.py
CHANGED
|
@@ -8,7 +8,6 @@ from .codebase import (
|
|
|
8
8
|
retrieve_code,
|
|
9
9
|
)
|
|
10
10
|
from .file_management import append_file, read_file, write_file
|
|
11
|
-
from .user_interaction import ask_user
|
|
12
11
|
from .web_search import (
|
|
13
12
|
anthropic_web_search_tool,
|
|
14
13
|
gemini_web_search_tool,
|
|
@@ -21,7 +20,6 @@ __all__ = [
|
|
|
21
20
|
"anthropic_web_search_tool",
|
|
22
21
|
"gemini_web_search_tool",
|
|
23
22
|
"get_available_web_search_tools",
|
|
24
|
-
"ask_user",
|
|
25
23
|
"read_file",
|
|
26
24
|
"write_file",
|
|
27
25
|
"append_file",
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from pydantic_ai import RunContext
|
|
9
9
|
|
|
10
10
|
from shotgun.agents.models import AgentDeps
|
|
11
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
11
12
|
from shotgun.logging_config import get_logger
|
|
12
13
|
|
|
13
14
|
from .models import ShellCommandResult
|
|
@@ -48,6 +49,11 @@ DANGEROUS_PATTERNS = [
|
|
|
48
49
|
]
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
@register_tool(
|
|
53
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
54
|
+
display_text="Running shell",
|
|
55
|
+
key_arg="command",
|
|
56
|
+
)
|
|
51
57
|
async def codebase_shell(
|
|
52
58
|
ctx: RunContext[AgentDeps],
|
|
53
59
|
command: str,
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from pydantic_ai import RunContext
|
|
6
6
|
|
|
7
7
|
from shotgun.agents.models import AgentDeps
|
|
8
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
8
9
|
from shotgun.logging_config import get_logger
|
|
9
10
|
|
|
10
11
|
from .models import DirectoryListResult
|
|
@@ -12,6 +13,11 @@ from .models import DirectoryListResult
|
|
|
12
13
|
logger = get_logger(__name__)
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
@register_tool(
|
|
17
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
18
|
+
display_text="Listing directory",
|
|
19
|
+
key_arg="directory",
|
|
20
|
+
)
|
|
15
21
|
async def directory_lister(
|
|
16
22
|
ctx: RunContext[AgentDeps], graph_id: str, directory: str = "."
|
|
17
23
|
) -> DirectoryListResult:
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from pydantic_ai import RunContext
|
|
6
6
|
|
|
7
7
|
from shotgun.agents.models import AgentDeps
|
|
8
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
8
9
|
from shotgun.codebase.core.language_config import get_language_config
|
|
9
10
|
from shotgun.logging_config import get_logger
|
|
10
11
|
|
|
@@ -13,6 +14,11 @@ from .models import FileReadResult
|
|
|
13
14
|
logger = get_logger(__name__)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
@register_tool(
|
|
18
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
19
|
+
display_text="Reading file",
|
|
20
|
+
key_arg="file_path",
|
|
21
|
+
)
|
|
16
22
|
async def file_read(
|
|
17
23
|
ctx: RunContext[AgentDeps], graph_id: str, file_path: str
|
|
18
24
|
) -> FileReadResult:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from pydantic_ai import RunContext
|
|
4
4
|
|
|
5
5
|
from shotgun.agents.models import AgentDeps
|
|
6
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
6
7
|
from shotgun.codebase.models import QueryType
|
|
7
8
|
from shotgun.logging_config import get_logger
|
|
8
9
|
|
|
@@ -11,6 +12,11 @@ from .models import QueryGraphResult
|
|
|
11
12
|
logger = get_logger(__name__)
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
@register_tool(
|
|
16
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
17
|
+
display_text="Querying code",
|
|
18
|
+
key_arg="query",
|
|
19
|
+
)
|
|
14
20
|
async def query_graph(
|
|
15
21
|
ctx: RunContext[AgentDeps], graph_id: str, query: str
|
|
16
22
|
) -> QueryGraphResult:
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from pydantic_ai import RunContext
|
|
6
6
|
|
|
7
7
|
from shotgun.agents.models import AgentDeps
|
|
8
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
8
9
|
from shotgun.codebase.core.code_retrieval import retrieve_code_by_qualified_name
|
|
9
10
|
from shotgun.codebase.core.language_config import get_language_config
|
|
10
11
|
from shotgun.logging_config import get_logger
|
|
@@ -14,6 +15,11 @@ from .models import CodeSnippetResult
|
|
|
14
15
|
logger = get_logger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
@register_tool(
|
|
19
|
+
category=ToolCategory.CODEBASE_UNDERSTANDING,
|
|
20
|
+
display_text="Retrieving code",
|
|
21
|
+
key_arg="qualified_name",
|
|
22
|
+
)
|
|
17
23
|
async def retrieve_code(
|
|
18
24
|
ctx: RunContext[AgentDeps], graph_id: str, qualified_name: str
|
|
19
25
|
) -> CodeSnippetResult:
|
|
@@ -9,17 +9,25 @@ from typing import Literal
|
|
|
9
9
|
from pydantic_ai import RunContext
|
|
10
10
|
|
|
11
11
|
from shotgun.agents.models import AgentDeps, AgentType, FileOperationType
|
|
12
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
12
13
|
from shotgun.logging_config import get_logger
|
|
13
14
|
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
14
15
|
|
|
15
16
|
logger = get_logger(__name__)
|
|
16
17
|
|
|
17
18
|
# Map agent modes to their allowed directories/files (in workflow order)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
# Values can be:
|
|
20
|
+
# - A Path: exact file (e.g., Path("research.md"))
|
|
21
|
+
# - A list of Paths: multiple allowed files/directories (e.g., [Path("specification.md"), Path("contracts")])
|
|
22
|
+
# - "*": any file except protected files (for export agent)
|
|
23
|
+
AGENT_DIRECTORIES: dict[AgentType, str | Path | list[Path]] = {
|
|
24
|
+
AgentType.RESEARCH: Path("research.md"),
|
|
25
|
+
AgentType.SPECIFY: [
|
|
26
|
+
Path("specification.md"),
|
|
27
|
+
Path("contracts"),
|
|
28
|
+
], # Specify can write specs and contract files
|
|
29
|
+
AgentType.PLAN: Path("plan.md"),
|
|
30
|
+
AgentType.TASKS: Path("tasks.md"),
|
|
23
31
|
AgentType.EXPORT: "*", # Export agent can write anywhere except protected files
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -60,13 +68,52 @@ def _validate_agent_scoped_path(filename: str, agent_mode: AgentType | None) ->
|
|
|
60
68
|
# Allow writing anywhere else in .shotgun directory
|
|
61
69
|
full_path = (base_path / filename).resolve()
|
|
62
70
|
else:
|
|
63
|
-
# For other agents,
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
# For other agents, check if they have access to the requested file
|
|
72
|
+
allowed_paths_raw = AGENT_DIRECTORIES[agent_mode]
|
|
73
|
+
|
|
74
|
+
# Convert single Path/string to list of Paths for uniform handling
|
|
75
|
+
if isinstance(allowed_paths_raw, str):
|
|
76
|
+
# Special case: "*" means export agent
|
|
77
|
+
allowed_paths = (
|
|
78
|
+
[Path(allowed_paths_raw)] if allowed_paths_raw != "*" else []
|
|
79
|
+
)
|
|
80
|
+
elif isinstance(allowed_paths_raw, Path):
|
|
81
|
+
allowed_paths = [allowed_paths_raw]
|
|
82
|
+
else:
|
|
83
|
+
# Already a list
|
|
84
|
+
allowed_paths = allowed_paths_raw
|
|
85
|
+
|
|
86
|
+
# Check if filename matches any allowed path
|
|
87
|
+
is_allowed = False
|
|
88
|
+
for allowed_path in allowed_paths:
|
|
89
|
+
allowed_str = str(allowed_path)
|
|
90
|
+
|
|
91
|
+
# Check if it's a directory (no .md extension or suffix)
|
|
92
|
+
# Directories: Path("contracts") has no suffix, files: Path("spec.md") has .md suffix
|
|
93
|
+
if not allowed_path.suffix or (
|
|
94
|
+
allowed_path.suffix and not allowed_str.endswith(".md")
|
|
95
|
+
):
|
|
96
|
+
# Directory - allow any file within this directory
|
|
97
|
+
# Check both "contracts/file.py" and "contracts" prefix
|
|
98
|
+
if (
|
|
99
|
+
filename.startswith(allowed_str + "/")
|
|
100
|
+
or filename == allowed_str
|
|
101
|
+
):
|
|
102
|
+
is_allowed = True
|
|
103
|
+
break
|
|
104
|
+
else:
|
|
105
|
+
# Exact file match
|
|
106
|
+
if filename == allowed_str:
|
|
107
|
+
is_allowed = True
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if not is_allowed:
|
|
111
|
+
allowed_display = ", ".join(f"'{p}'" for p in allowed_paths)
|
|
66
112
|
raise ValueError(
|
|
67
|
-
f"{agent_mode.value.capitalize()} agent can only write to
|
|
113
|
+
f"{agent_mode.value.capitalize()} agent can only write to {allowed_display}. "
|
|
68
114
|
f"Attempted to write to '{filename}'"
|
|
69
115
|
)
|
|
116
|
+
|
|
70
117
|
full_path = (base_path / filename).resolve()
|
|
71
118
|
else:
|
|
72
119
|
# No agent mode specified, fall back to old validation
|
|
@@ -111,6 +158,11 @@ def _validate_shotgun_path(filename: str) -> Path:
|
|
|
111
158
|
return full_path
|
|
112
159
|
|
|
113
160
|
|
|
161
|
+
@register_tool(
|
|
162
|
+
category=ToolCategory.ARTIFACT_MANAGEMENT,
|
|
163
|
+
display_text="Reading file",
|
|
164
|
+
key_arg="filename",
|
|
165
|
+
)
|
|
114
166
|
async def read_file(ctx: RunContext[AgentDeps], filename: str) -> str:
|
|
115
167
|
"""Read a file from the .shotgun directory.
|
|
116
168
|
|
|
@@ -142,6 +194,11 @@ async def read_file(ctx: RunContext[AgentDeps], filename: str) -> str:
|
|
|
142
194
|
return error_msg
|
|
143
195
|
|
|
144
196
|
|
|
197
|
+
@register_tool(
|
|
198
|
+
category=ToolCategory.ARTIFACT_MANAGEMENT,
|
|
199
|
+
display_text="Writing file",
|
|
200
|
+
key_arg="filename",
|
|
201
|
+
)
|
|
145
202
|
async def write_file(
|
|
146
203
|
ctx: RunContext[AgentDeps],
|
|
147
204
|
filename: str,
|
|
@@ -205,6 +262,11 @@ async def write_file(
|
|
|
205
262
|
return error_msg
|
|
206
263
|
|
|
207
264
|
|
|
265
|
+
@register_tool(
|
|
266
|
+
category=ToolCategory.ARTIFACT_MANAGEMENT,
|
|
267
|
+
display_text="Appending to file",
|
|
268
|
+
key_arg="filename",
|
|
269
|
+
)
|
|
208
270
|
async def append_file(ctx: RunContext[AgentDeps], filename: str, content: str) -> str:
|
|
209
271
|
"""Append content to a file in the .shotgun directory.
|
|
210
272
|
|