shotgun-sh 0.1.0__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/__init__.py +5 -0
- shotgun/agents/__init__.py +1 -0
- shotgun/agents/agent_manager.py +651 -0
- shotgun/agents/common.py +549 -0
- shotgun/agents/config/__init__.py +13 -0
- shotgun/agents/config/constants.py +17 -0
- shotgun/agents/config/manager.py +294 -0
- shotgun/agents/config/models.py +185 -0
- shotgun/agents/config/provider.py +206 -0
- shotgun/agents/conversation_history.py +106 -0
- shotgun/agents/conversation_manager.py +105 -0
- shotgun/agents/export.py +96 -0
- shotgun/agents/history/__init__.py +5 -0
- shotgun/agents/history/compaction.py +85 -0
- shotgun/agents/history/constants.py +19 -0
- shotgun/agents/history/context_extraction.py +108 -0
- shotgun/agents/history/history_building.py +104 -0
- shotgun/agents/history/history_processors.py +426 -0
- shotgun/agents/history/message_utils.py +84 -0
- shotgun/agents/history/token_counting.py +429 -0
- shotgun/agents/history/token_estimation.py +138 -0
- shotgun/agents/messages.py +35 -0
- shotgun/agents/models.py +275 -0
- shotgun/agents/plan.py +98 -0
- shotgun/agents/research.py +108 -0
- shotgun/agents/specify.py +98 -0
- shotgun/agents/tasks.py +96 -0
- shotgun/agents/tools/__init__.py +34 -0
- shotgun/agents/tools/codebase/__init__.py +28 -0
- shotgun/agents/tools/codebase/codebase_shell.py +256 -0
- shotgun/agents/tools/codebase/directory_lister.py +141 -0
- shotgun/agents/tools/codebase/file_read.py +144 -0
- shotgun/agents/tools/codebase/models.py +252 -0
- shotgun/agents/tools/codebase/query_graph.py +67 -0
- shotgun/agents/tools/codebase/retrieve_code.py +81 -0
- shotgun/agents/tools/file_management.py +218 -0
- shotgun/agents/tools/user_interaction.py +37 -0
- shotgun/agents/tools/web_search/__init__.py +60 -0
- shotgun/agents/tools/web_search/anthropic.py +144 -0
- shotgun/agents/tools/web_search/gemini.py +85 -0
- shotgun/agents/tools/web_search/openai.py +98 -0
- shotgun/agents/tools/web_search/utils.py +20 -0
- shotgun/build_constants.py +20 -0
- shotgun/cli/__init__.py +1 -0
- shotgun/cli/codebase/__init__.py +5 -0
- shotgun/cli/codebase/commands.py +202 -0
- shotgun/cli/codebase/models.py +21 -0
- shotgun/cli/config.py +275 -0
- shotgun/cli/export.py +81 -0
- shotgun/cli/models.py +10 -0
- shotgun/cli/plan.py +73 -0
- shotgun/cli/research.py +85 -0
- shotgun/cli/specify.py +69 -0
- shotgun/cli/tasks.py +78 -0
- shotgun/cli/update.py +152 -0
- shotgun/cli/utils.py +25 -0
- shotgun/codebase/__init__.py +12 -0
- shotgun/codebase/core/__init__.py +46 -0
- shotgun/codebase/core/change_detector.py +358 -0
- shotgun/codebase/core/code_retrieval.py +243 -0
- shotgun/codebase/core/ingestor.py +1497 -0
- shotgun/codebase/core/language_config.py +297 -0
- shotgun/codebase/core/manager.py +1662 -0
- shotgun/codebase/core/nl_query.py +331 -0
- shotgun/codebase/core/parser_loader.py +128 -0
- shotgun/codebase/models.py +111 -0
- shotgun/codebase/service.py +206 -0
- shotgun/logging_config.py +227 -0
- shotgun/main.py +167 -0
- shotgun/posthog_telemetry.py +158 -0
- shotgun/prompts/__init__.py +5 -0
- shotgun/prompts/agents/__init__.py +1 -0
- shotgun/prompts/agents/export.j2 +350 -0
- shotgun/prompts/agents/partials/codebase_understanding.j2 +87 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +37 -0
- shotgun/prompts/agents/partials/content_formatting.j2 +65 -0
- shotgun/prompts/agents/partials/interactive_mode.j2 +26 -0
- shotgun/prompts/agents/plan.j2 +144 -0
- shotgun/prompts/agents/research.j2 +69 -0
- shotgun/prompts/agents/specify.j2 +51 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +19 -0
- shotgun/prompts/agents/state/system_state.j2 +31 -0
- shotgun/prompts/agents/tasks.j2 +143 -0
- shotgun/prompts/codebase/__init__.py +1 -0
- shotgun/prompts/codebase/cypher_query_patterns.j2 +223 -0
- shotgun/prompts/codebase/cypher_system.j2 +28 -0
- shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
- shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
- shotgun/prompts/codebase/partials/graph_schema.j2 +30 -0
- shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
- shotgun/prompts/history/__init__.py +1 -0
- shotgun/prompts/history/incremental_summarization.j2 +53 -0
- shotgun/prompts/history/summarization.j2 +46 -0
- shotgun/prompts/loader.py +140 -0
- shotgun/py.typed +0 -0
- shotgun/sdk/__init__.py +13 -0
- shotgun/sdk/codebase.py +219 -0
- shotgun/sdk/exceptions.py +17 -0
- shotgun/sdk/models.py +189 -0
- shotgun/sdk/services.py +23 -0
- shotgun/sentry_telemetry.py +87 -0
- shotgun/telemetry.py +93 -0
- shotgun/tui/__init__.py +0 -0
- shotgun/tui/app.py +116 -0
- shotgun/tui/commands/__init__.py +76 -0
- shotgun/tui/components/prompt_input.py +69 -0
- shotgun/tui/components/spinner.py +86 -0
- shotgun/tui/components/splash.py +25 -0
- shotgun/tui/components/vertical_tail.py +13 -0
- shotgun/tui/screens/chat.py +782 -0
- shotgun/tui/screens/chat.tcss +43 -0
- shotgun/tui/screens/chat_screen/__init__.py +0 -0
- shotgun/tui/screens/chat_screen/command_providers.py +219 -0
- shotgun/tui/screens/chat_screen/hint_message.py +40 -0
- shotgun/tui/screens/chat_screen/history.py +221 -0
- shotgun/tui/screens/directory_setup.py +113 -0
- shotgun/tui/screens/provider_config.py +221 -0
- shotgun/tui/screens/splash.py +31 -0
- shotgun/tui/styles.tcss +10 -0
- shotgun/tui/utils/__init__.py +5 -0
- shotgun/tui/utils/mode_progress.py +257 -0
- shotgun/utils/__init__.py +5 -0
- shotgun/utils/env_utils.py +35 -0
- shotgun/utils/file_system_utils.py +36 -0
- shotgun/utils/update_checker.py +375 -0
- shotgun_sh-0.1.0.dist-info/METADATA +466 -0
- shotgun_sh-0.1.0.dist-info/RECORD +130 -0
- shotgun_sh-0.1.0.dist-info/WHEEL +4 -0
- shotgun_sh-0.1.0.dist-info/entry_points.txt +2 -0
- shotgun_sh-0.1.0.dist-info/licenses/LICENSE +21 -0
shotgun/agents/common.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""Common utilities for agent creation and management."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic_ai import (
|
|
9
|
+
Agent,
|
|
10
|
+
DeferredToolRequests,
|
|
11
|
+
DeferredToolResults,
|
|
12
|
+
RunContext,
|
|
13
|
+
UsageLimits,
|
|
14
|
+
)
|
|
15
|
+
from pydantic_ai.agent import AgentRunResult
|
|
16
|
+
from pydantic_ai.messages import (
|
|
17
|
+
ModelMessage,
|
|
18
|
+
ModelRequest,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from shotgun.agents.config import ProviderType, get_config_manager, get_provider_model
|
|
22
|
+
from shotgun.agents.models import AgentType
|
|
23
|
+
from shotgun.logging_config import get_logger
|
|
24
|
+
from shotgun.prompts import PromptLoader
|
|
25
|
+
from shotgun.sdk.services import get_codebase_service
|
|
26
|
+
from shotgun.utils import ensure_shotgun_directory_exists
|
|
27
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
28
|
+
|
|
29
|
+
from .history import token_limit_compactor
|
|
30
|
+
from .history.compaction import apply_persistent_compaction
|
|
31
|
+
from .messages import AgentSystemPrompt, SystemStatusPrompt
|
|
32
|
+
from .models import AgentDeps, AgentRuntimeOptions, PipelineConfigEntry
|
|
33
|
+
from .tools import (
|
|
34
|
+
append_file,
|
|
35
|
+
ask_user,
|
|
36
|
+
codebase_shell,
|
|
37
|
+
directory_lister,
|
|
38
|
+
file_read,
|
|
39
|
+
query_graph,
|
|
40
|
+
read_file,
|
|
41
|
+
retrieve_code,
|
|
42
|
+
write_file,
|
|
43
|
+
)
|
|
44
|
+
from .tools.file_management import AGENT_DIRECTORIES
|
|
45
|
+
|
|
46
|
+
logger = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
# Global prompt loader instance
|
|
49
|
+
prompt_loader = PromptLoader()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def add_system_status_message(
|
|
53
|
+
deps: AgentDeps,
|
|
54
|
+
message_history: list[ModelMessage] | None = None,
|
|
55
|
+
) -> list[ModelMessage]:
|
|
56
|
+
"""Add a system status message to the message history.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
deps: Agent dependencies containing runtime options
|
|
60
|
+
message_history: Existing message history
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Updated message history with system status message prepended
|
|
64
|
+
"""
|
|
65
|
+
message_history = message_history or []
|
|
66
|
+
# Only show graphs for the current working directory
|
|
67
|
+
codebase_understanding_graphs = (
|
|
68
|
+
await deps.codebase_service.list_graphs_for_directory()
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Get existing files for the agent
|
|
72
|
+
existing_files = get_agent_existing_files(deps.agent_mode)
|
|
73
|
+
|
|
74
|
+
# Extract table of contents from the agent's markdown file
|
|
75
|
+
markdown_toc = extract_markdown_toc(deps.agent_mode)
|
|
76
|
+
|
|
77
|
+
system_state = prompt_loader.render(
|
|
78
|
+
"agents/state/system_state.j2",
|
|
79
|
+
codebase_understanding_graphs=codebase_understanding_graphs,
|
|
80
|
+
is_tui_context=deps.is_tui_context,
|
|
81
|
+
existing_files=existing_files,
|
|
82
|
+
markdown_toc=markdown_toc,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
message_history.append(
|
|
86
|
+
ModelRequest(
|
|
87
|
+
parts=[
|
|
88
|
+
SystemStatusPrompt(content=system_state),
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return message_history
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def create_base_agent(
|
|
96
|
+
system_prompt_fn: Callable[[RunContext[AgentDeps]], str],
|
|
97
|
+
agent_runtime_options: AgentRuntimeOptions,
|
|
98
|
+
load_codebase_understanding_tools: bool = True,
|
|
99
|
+
additional_tools: list[Any] | None = None,
|
|
100
|
+
provider: ProviderType | None = None,
|
|
101
|
+
agent_mode: AgentType | None = None,
|
|
102
|
+
) -> tuple[Agent[AgentDeps, str | DeferredToolRequests], AgentDeps]:
|
|
103
|
+
"""Create a base agent with common configuration.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
system_prompt_fn: Function that will be decorated as system_prompt
|
|
107
|
+
agent_runtime_options: Agent runtime options for the agent
|
|
108
|
+
load_codebase_understanding_tools: Whether to load codebase understanding tools
|
|
109
|
+
additional_tools: Optional list of additional tools
|
|
110
|
+
provider: Optional provider override. If None, uses configured default
|
|
111
|
+
agent_mode: The mode of the agent (research, plan, tasks, specify, export)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Tuple of (Configured Pydantic AI agent, Agent dependencies)
|
|
115
|
+
"""
|
|
116
|
+
ensure_shotgun_directory_exists()
|
|
117
|
+
|
|
118
|
+
# Get configured model or fall back to hardcoded default
|
|
119
|
+
try:
|
|
120
|
+
model_config = get_provider_model(provider)
|
|
121
|
+
config_manager = get_config_manager()
|
|
122
|
+
provider_name = provider or config_manager.load().default_provider
|
|
123
|
+
logger.debug(
|
|
124
|
+
"🤖 Creating agent with configured %s model: %s",
|
|
125
|
+
provider_name.upper(),
|
|
126
|
+
model_config.name,
|
|
127
|
+
)
|
|
128
|
+
# Use the Model instance directly (has API key baked in)
|
|
129
|
+
model = model_config.model_instance
|
|
130
|
+
|
|
131
|
+
# Create deps with model config and services
|
|
132
|
+
codebase_service = get_codebase_service()
|
|
133
|
+
deps = AgentDeps(
|
|
134
|
+
**agent_runtime_options.model_dump(),
|
|
135
|
+
llm_model=model_config,
|
|
136
|
+
codebase_service=codebase_service,
|
|
137
|
+
system_prompt_fn=system_prompt_fn,
|
|
138
|
+
agent_mode=agent_mode,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning("Failed to load configured model, using fallback: %s", e)
|
|
143
|
+
logger.debug("🤖 Creating agent with fallback OpenAI GPT-4o")
|
|
144
|
+
raise ValueError("Configured model is required") from e
|
|
145
|
+
|
|
146
|
+
# Create a history processor that has access to deps via closure
|
|
147
|
+
async def history_processor(messages: list[ModelMessage]) -> list[ModelMessage]:
|
|
148
|
+
"""History processor with access to deps via closure."""
|
|
149
|
+
|
|
150
|
+
# Create a minimal context for compaction
|
|
151
|
+
class ProcessorContext:
|
|
152
|
+
def __init__(self, deps: AgentDeps):
|
|
153
|
+
self.deps = deps
|
|
154
|
+
self.usage = None # Will be estimated from messages
|
|
155
|
+
|
|
156
|
+
ctx = ProcessorContext(deps)
|
|
157
|
+
return await token_limit_compactor(ctx, messages)
|
|
158
|
+
|
|
159
|
+
agent = Agent(
|
|
160
|
+
model,
|
|
161
|
+
output_type=[str, DeferredToolRequests],
|
|
162
|
+
deps_type=AgentDeps,
|
|
163
|
+
instrument=True,
|
|
164
|
+
history_processors=[history_processor],
|
|
165
|
+
retries=3, # Default retry count for tool calls and output validation
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# System prompt function is stored in deps and will be called manually in run_agent
|
|
169
|
+
func_name = getattr(system_prompt_fn, "__name__", str(system_prompt_fn))
|
|
170
|
+
logger.debug("🔧 System prompt function stored: %s", func_name)
|
|
171
|
+
|
|
172
|
+
# Register additional tools first (agent-specific)
|
|
173
|
+
for tool in additional_tools or []:
|
|
174
|
+
agent.tool_plain(tool)
|
|
175
|
+
|
|
176
|
+
# Register interactive tool conditionally based on deps
|
|
177
|
+
if deps.interactive_mode:
|
|
178
|
+
agent.tool(ask_user)
|
|
179
|
+
logger.debug("📞 Interactive mode enabled - ask_user tool registered")
|
|
180
|
+
|
|
181
|
+
# Register common file management tools (always available)
|
|
182
|
+
agent.tool(write_file)
|
|
183
|
+
agent.tool(append_file)
|
|
184
|
+
agent.tool(read_file)
|
|
185
|
+
|
|
186
|
+
# Register codebase understanding tools (conditional)
|
|
187
|
+
if load_codebase_understanding_tools:
|
|
188
|
+
agent.tool(query_graph)
|
|
189
|
+
agent.tool(retrieve_code)
|
|
190
|
+
agent.tool(file_read)
|
|
191
|
+
agent.tool(directory_lister)
|
|
192
|
+
agent.tool(codebase_shell)
|
|
193
|
+
logger.debug("🧠 Codebase understanding tools registered")
|
|
194
|
+
else:
|
|
195
|
+
logger.debug("🚫🧠 Codebase understanding tools not registered")
|
|
196
|
+
|
|
197
|
+
logger.debug("✅ Agent creation complete with codebase tools")
|
|
198
|
+
return agent, deps
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _extract_file_toc_content(
|
|
202
|
+
file_path: Path, max_depth: int | None = None, max_chars: int = 500
|
|
203
|
+
) -> str | None:
|
|
204
|
+
"""Extract TOC from a single file with depth and character limits.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
file_path: Path to the markdown file
|
|
208
|
+
max_depth: Maximum heading depth (1=#, 2=##, None=all)
|
|
209
|
+
max_chars: Maximum characters for the TOC
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Formatted TOC string or None if file doesn't exist
|
|
213
|
+
"""
|
|
214
|
+
if not file_path.exists():
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
content = file_path.read_text(encoding="utf-8")
|
|
219
|
+
lines = content.split("\n")
|
|
220
|
+
|
|
221
|
+
# Extract headings
|
|
222
|
+
toc_lines = []
|
|
223
|
+
for line in lines:
|
|
224
|
+
stripped = line.strip()
|
|
225
|
+
if stripped.startswith("#"):
|
|
226
|
+
# Count the heading level
|
|
227
|
+
level = 0
|
|
228
|
+
for char in stripped:
|
|
229
|
+
if char == "#":
|
|
230
|
+
level += 1
|
|
231
|
+
else:
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
# Skip if exceeds max_depth
|
|
235
|
+
if max_depth and level > max_depth:
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
# Get the heading text (remove the # symbols and clean up)
|
|
239
|
+
heading_text = stripped[level:].strip()
|
|
240
|
+
if heading_text:
|
|
241
|
+
# Add indentation based on level
|
|
242
|
+
indent = " " * (level - 1)
|
|
243
|
+
toc_lines.append(f"{indent}{'#' * level} {heading_text}")
|
|
244
|
+
|
|
245
|
+
# Check if we're approaching the character limit
|
|
246
|
+
current_length = sum(len(line) + 1 for line in toc_lines)
|
|
247
|
+
if current_length > max_chars:
|
|
248
|
+
# Remove the last line and add ellipsis
|
|
249
|
+
toc_lines.pop()
|
|
250
|
+
if toc_lines:
|
|
251
|
+
toc_lines.append(" ...")
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
if not toc_lines:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
return "\n".join(toc_lines)
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.debug(f"Failed to extract TOC from {file_path}: {e}")
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def extract_markdown_toc(agent_mode: AgentType | None) -> str | None:
|
|
265
|
+
"""Extract TOCs from current and prior agents' files in the pipeline.
|
|
266
|
+
|
|
267
|
+
Shows full TOC of agent's own file and high-level summaries of prior agents'
|
|
268
|
+
files to maintain context awareness while keeping context window tight.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
agent_mode: The agent mode to extract TOC for
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Formatted multi-file TOC string or None if not applicable
|
|
275
|
+
"""
|
|
276
|
+
# Skip if no mode
|
|
277
|
+
if not agent_mode:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
# Define pipeline order and dependencies
|
|
281
|
+
pipeline_config: dict[AgentType, PipelineConfigEntry] = {
|
|
282
|
+
AgentType.RESEARCH: PipelineConfigEntry(
|
|
283
|
+
own_file="research.md",
|
|
284
|
+
prior_files=[], # First in pipeline
|
|
285
|
+
),
|
|
286
|
+
AgentType.SPECIFY: PipelineConfigEntry(
|
|
287
|
+
own_file="specification.md",
|
|
288
|
+
prior_files=["research.md"],
|
|
289
|
+
),
|
|
290
|
+
AgentType.PLAN: PipelineConfigEntry(
|
|
291
|
+
own_file="plan.md",
|
|
292
|
+
prior_files=["research.md", "specification.md"],
|
|
293
|
+
),
|
|
294
|
+
AgentType.TASKS: PipelineConfigEntry(
|
|
295
|
+
own_file="tasks.md",
|
|
296
|
+
prior_files=["research.md", "specification.md", "plan.md"],
|
|
297
|
+
),
|
|
298
|
+
AgentType.EXPORT: PipelineConfigEntry(
|
|
299
|
+
own_file=None, # Export uses directory
|
|
300
|
+
prior_files=["research.md", "specification.md", "plan.md", "tasks.md"],
|
|
301
|
+
),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Get configuration for current agent
|
|
305
|
+
if agent_mode not in pipeline_config:
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
config = pipeline_config[agent_mode]
|
|
309
|
+
base_path = get_shotgun_base_path()
|
|
310
|
+
toc_sections: list[str] = []
|
|
311
|
+
|
|
312
|
+
# Extract TOCs from prior files (high-level only)
|
|
313
|
+
for prior_file in config.prior_files:
|
|
314
|
+
file_path = base_path / prior_file
|
|
315
|
+
# Only show # and ## headings from prior files, max 500 chars each
|
|
316
|
+
prior_toc = _extract_file_toc_content(file_path, max_depth=2, max_chars=500)
|
|
317
|
+
if prior_toc:
|
|
318
|
+
# Add section with XML tags
|
|
319
|
+
toc_sections.append(
|
|
320
|
+
f'<TABLE_OF_CONTENTS file_name="{prior_file}">\n{prior_toc}\n</TABLE_OF_CONTENTS>'
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Extract TOC from own file (full detail)
|
|
324
|
+
if config.own_file:
|
|
325
|
+
own_path = base_path / config.own_file
|
|
326
|
+
own_toc = _extract_file_toc_content(own_path, max_depth=None, max_chars=2000)
|
|
327
|
+
if own_toc:
|
|
328
|
+
# Put own file TOC at the beginning with XML tags
|
|
329
|
+
toc_sections.insert(
|
|
330
|
+
0,
|
|
331
|
+
f'<TABLE_OF_CONTENTS file_name="{config.own_file}">\n{own_toc}\n</TABLE_OF_CONTENTS>',
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Combine all sections
|
|
335
|
+
if not toc_sections:
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
combined_toc = "\n\n".join(toc_sections)
|
|
339
|
+
|
|
340
|
+
# Final truncation if needed (should rarely happen with our limits)
|
|
341
|
+
max_total = 3500 # Conservative total limit
|
|
342
|
+
if len(combined_toc) > max_total:
|
|
343
|
+
combined_toc = combined_toc[: max_total - 3] + "..."
|
|
344
|
+
|
|
345
|
+
return combined_toc
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def get_agent_existing_files(agent_mode: AgentType | None = None) -> list[str]:
|
|
349
|
+
"""Get list of existing files for the given agent mode.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
agent_mode: The agent mode to check files for. If None, lists all files.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
List of existing file paths relative to .shotgun directory
|
|
356
|
+
"""
|
|
357
|
+
base_path = get_shotgun_base_path()
|
|
358
|
+
existing_files = []
|
|
359
|
+
|
|
360
|
+
# If no agent mode, list all files in base path and first level subdirectories
|
|
361
|
+
if agent_mode is None:
|
|
362
|
+
# List files in the root .shotgun directory
|
|
363
|
+
for item in base_path.iterdir():
|
|
364
|
+
if item.is_file():
|
|
365
|
+
existing_files.append(item.name)
|
|
366
|
+
elif item.is_dir():
|
|
367
|
+
# List files in first-level subdirectories
|
|
368
|
+
for subitem in item.iterdir():
|
|
369
|
+
if subitem.is_file():
|
|
370
|
+
relative_path = subitem.relative_to(base_path)
|
|
371
|
+
existing_files.append(str(relative_path))
|
|
372
|
+
return existing_files
|
|
373
|
+
|
|
374
|
+
# Handle specific agent modes
|
|
375
|
+
if agent_mode not in AGENT_DIRECTORIES:
|
|
376
|
+
return []
|
|
377
|
+
|
|
378
|
+
if agent_mode == AgentType.EXPORT:
|
|
379
|
+
# For export agent, list all files in exports directory
|
|
380
|
+
exports_dir = base_path / "exports"
|
|
381
|
+
if exports_dir.exists():
|
|
382
|
+
for file_path in exports_dir.rglob("*"):
|
|
383
|
+
if file_path.is_file():
|
|
384
|
+
relative_path = file_path.relative_to(base_path)
|
|
385
|
+
existing_files.append(str(relative_path))
|
|
386
|
+
else:
|
|
387
|
+
# For other agents, check both .md file and directory with same name
|
|
388
|
+
allowed_file = AGENT_DIRECTORIES[agent_mode]
|
|
389
|
+
|
|
390
|
+
# Check for the .md file
|
|
391
|
+
md_file_path = base_path / allowed_file
|
|
392
|
+
if md_file_path.exists():
|
|
393
|
+
existing_files.append(allowed_file)
|
|
394
|
+
|
|
395
|
+
# Check for directory with same base name (e.g., research/ for research.md)
|
|
396
|
+
base_name = allowed_file.replace(".md", "")
|
|
397
|
+
dir_path = base_path / base_name
|
|
398
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
399
|
+
# List all files in the directory
|
|
400
|
+
for file_path in dir_path.rglob("*"):
|
|
401
|
+
if file_path.is_file():
|
|
402
|
+
relative_path = file_path.relative_to(base_path)
|
|
403
|
+
existing_files.append(str(relative_path))
|
|
404
|
+
|
|
405
|
+
return existing_files
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def build_agent_system_prompt(
|
|
409
|
+
agent_type: str,
|
|
410
|
+
ctx: RunContext[AgentDeps],
|
|
411
|
+
context_name: str | None = None,
|
|
412
|
+
) -> str:
|
|
413
|
+
"""Build system prompt for any agent type.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
agent_type: Type of agent ('research', 'plan', 'tasks')
|
|
417
|
+
ctx: RunContext containing AgentDeps
|
|
418
|
+
context_name: Optional context name for template rendering
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Rendered system prompt
|
|
422
|
+
"""
|
|
423
|
+
prompt_loader = PromptLoader()
|
|
424
|
+
|
|
425
|
+
# Add logging if research agent
|
|
426
|
+
if agent_type == "research":
|
|
427
|
+
logger.debug("🔧 Building research agent system prompt...")
|
|
428
|
+
logger.debug("Interactive mode: %s", ctx.deps.interactive_mode)
|
|
429
|
+
|
|
430
|
+
result = prompt_loader.render(
|
|
431
|
+
f"agents/{agent_type}.j2",
|
|
432
|
+
interactive_mode=ctx.deps.interactive_mode,
|
|
433
|
+
mode=agent_type,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
if agent_type == "research":
|
|
437
|
+
logger.debug(
|
|
438
|
+
"✅ Research system prompt built successfully (length: %d chars)",
|
|
439
|
+
len(result),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return result
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def create_usage_limits() -> UsageLimits:
|
|
446
|
+
"""Create reasonable usage limits for agent runs.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
UsageLimits configured for responsible API usage
|
|
450
|
+
"""
|
|
451
|
+
return UsageLimits(
|
|
452
|
+
request_limit=100, # Maximum number of model requests per run
|
|
453
|
+
tool_calls_limit=100, # Maximum number of successful tool calls
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
async def add_system_prompt_message(
|
|
458
|
+
deps: AgentDeps,
|
|
459
|
+
message_history: list[ModelMessage] | None = None,
|
|
460
|
+
) -> list[ModelMessage]:
|
|
461
|
+
"""Add the system prompt as the first message in the message history.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
deps: Agent dependencies containing system_prompt_fn
|
|
465
|
+
message_history: Existing message history
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Updated message history with system prompt prepended as first message
|
|
469
|
+
"""
|
|
470
|
+
message_history = message_history or []
|
|
471
|
+
|
|
472
|
+
# Create a minimal RunContext to call the system prompt function
|
|
473
|
+
# We'll pass None for model and usage since they're not used by our system prompt functions
|
|
474
|
+
context = type(
|
|
475
|
+
"RunContext", (), {"deps": deps, "retry": 0, "model": None, "usage": None}
|
|
476
|
+
)()
|
|
477
|
+
|
|
478
|
+
# Render the system prompt using the stored function
|
|
479
|
+
system_prompt_content = deps.system_prompt_fn(context)
|
|
480
|
+
logger.debug(
|
|
481
|
+
"🎯 Rendered system prompt (length: %d chars)", len(system_prompt_content)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Create system message and prepend to message history
|
|
485
|
+
system_message = ModelRequest(
|
|
486
|
+
parts=[
|
|
487
|
+
AgentSystemPrompt(content=system_prompt_content, agent_mode=deps.agent_mode)
|
|
488
|
+
]
|
|
489
|
+
)
|
|
490
|
+
message_history.insert(0, system_message)
|
|
491
|
+
logger.debug("✅ System prompt prepended as first message")
|
|
492
|
+
|
|
493
|
+
return message_history
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
async def run_agent(
|
|
497
|
+
agent: Agent[AgentDeps, str | DeferredToolRequests],
|
|
498
|
+
prompt: str,
|
|
499
|
+
deps: AgentDeps,
|
|
500
|
+
message_history: list[ModelMessage] | None = None,
|
|
501
|
+
usage_limits: UsageLimits | None = None,
|
|
502
|
+
) -> AgentRunResult[str | DeferredToolRequests]:
|
|
503
|
+
# Clear file tracker for new run
|
|
504
|
+
deps.file_tracker.clear()
|
|
505
|
+
logger.debug("🔧 Cleared file tracker for new agent run")
|
|
506
|
+
|
|
507
|
+
# Add system prompt as first message
|
|
508
|
+
message_history = await add_system_prompt_message(deps, message_history)
|
|
509
|
+
|
|
510
|
+
result = await agent.run(
|
|
511
|
+
prompt,
|
|
512
|
+
deps=deps,
|
|
513
|
+
usage_limits=usage_limits,
|
|
514
|
+
message_history=message_history,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Apply persistent compaction to prevent cascading token growth across CLI commands
|
|
518
|
+
messages = await apply_persistent_compaction(result.all_messages(), deps)
|
|
519
|
+
while isinstance(result.output, DeferredToolRequests):
|
|
520
|
+
logger.info("got deferred tool requests")
|
|
521
|
+
await deps.queue.join()
|
|
522
|
+
requests = result.output
|
|
523
|
+
done, _ = await asyncio.wait(deps.tasks)
|
|
524
|
+
|
|
525
|
+
task_results = [task.result() for task in done]
|
|
526
|
+
task_results_by_tool_call_id = {
|
|
527
|
+
result.tool_call_id: result.answer for result in task_results
|
|
528
|
+
}
|
|
529
|
+
logger.info("got task results", task_results_by_tool_call_id)
|
|
530
|
+
results = DeferredToolResults()
|
|
531
|
+
for call in requests.calls:
|
|
532
|
+
results.calls[call.tool_call_id] = task_results_by_tool_call_id[
|
|
533
|
+
call.tool_call_id
|
|
534
|
+
]
|
|
535
|
+
result = await agent.run(
|
|
536
|
+
deps=deps,
|
|
537
|
+
usage_limits=usage_limits,
|
|
538
|
+
message_history=messages,
|
|
539
|
+
deferred_tool_results=results,
|
|
540
|
+
)
|
|
541
|
+
# Apply persistent compaction to prevent cascading token growth in multi-turn loops
|
|
542
|
+
messages = await apply_persistent_compaction(result.all_messages(), deps)
|
|
543
|
+
|
|
544
|
+
# Log file operations summary if any files were modified
|
|
545
|
+
if deps.file_tracker.operations:
|
|
546
|
+
summary = deps.file_tracker.format_summary()
|
|
547
|
+
logger.info("📁 %s", summary)
|
|
548
|
+
|
|
549
|
+
return result
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Configuration module for Shotgun CLI."""
|
|
2
|
+
|
|
3
|
+
from .manager import ConfigManager, get_config_manager
|
|
4
|
+
from .models import ProviderType, ShotgunConfig
|
|
5
|
+
from .provider import get_provider_model
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ConfigManager",
|
|
9
|
+
"get_config_manager",
|
|
10
|
+
"ProviderType",
|
|
11
|
+
"ShotgunConfig",
|
|
12
|
+
"get_provider_model",
|
|
13
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Configuration constants for Shotgun agents."""
|
|
2
|
+
|
|
3
|
+
# Field names
|
|
4
|
+
API_KEY_FIELD = "api_key"
|
|
5
|
+
DEFAULT_PROVIDER_FIELD = "default_provider"
|
|
6
|
+
USER_ID_FIELD = "user_id"
|
|
7
|
+
CONFIG_VERSION_FIELD = "config_version"
|
|
8
|
+
|
|
9
|
+
# Provider names (for consistency with data dict keys)
|
|
10
|
+
OPENAI_PROVIDER = "openai"
|
|
11
|
+
ANTHROPIC_PROVIDER = "anthropic"
|
|
12
|
+
GOOGLE_PROVIDER = "google"
|
|
13
|
+
|
|
14
|
+
# Environment variable names
|
|
15
|
+
OPENAI_API_KEY_ENV = "OPENAI_API_KEY"
|
|
16
|
+
ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY"
|
|
17
|
+
GEMINI_API_KEY_ENV = "GEMINI_API_KEY"
|