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.

Files changed (130) hide show
  1. shotgun/__init__.py +5 -0
  2. shotgun/agents/__init__.py +1 -0
  3. shotgun/agents/agent_manager.py +651 -0
  4. shotgun/agents/common.py +549 -0
  5. shotgun/agents/config/__init__.py +13 -0
  6. shotgun/agents/config/constants.py +17 -0
  7. shotgun/agents/config/manager.py +294 -0
  8. shotgun/agents/config/models.py +185 -0
  9. shotgun/agents/config/provider.py +206 -0
  10. shotgun/agents/conversation_history.py +106 -0
  11. shotgun/agents/conversation_manager.py +105 -0
  12. shotgun/agents/export.py +96 -0
  13. shotgun/agents/history/__init__.py +5 -0
  14. shotgun/agents/history/compaction.py +85 -0
  15. shotgun/agents/history/constants.py +19 -0
  16. shotgun/agents/history/context_extraction.py +108 -0
  17. shotgun/agents/history/history_building.py +104 -0
  18. shotgun/agents/history/history_processors.py +426 -0
  19. shotgun/agents/history/message_utils.py +84 -0
  20. shotgun/agents/history/token_counting.py +429 -0
  21. shotgun/agents/history/token_estimation.py +138 -0
  22. shotgun/agents/messages.py +35 -0
  23. shotgun/agents/models.py +275 -0
  24. shotgun/agents/plan.py +98 -0
  25. shotgun/agents/research.py +108 -0
  26. shotgun/agents/specify.py +98 -0
  27. shotgun/agents/tasks.py +96 -0
  28. shotgun/agents/tools/__init__.py +34 -0
  29. shotgun/agents/tools/codebase/__init__.py +28 -0
  30. shotgun/agents/tools/codebase/codebase_shell.py +256 -0
  31. shotgun/agents/tools/codebase/directory_lister.py +141 -0
  32. shotgun/agents/tools/codebase/file_read.py +144 -0
  33. shotgun/agents/tools/codebase/models.py +252 -0
  34. shotgun/agents/tools/codebase/query_graph.py +67 -0
  35. shotgun/agents/tools/codebase/retrieve_code.py +81 -0
  36. shotgun/agents/tools/file_management.py +218 -0
  37. shotgun/agents/tools/user_interaction.py +37 -0
  38. shotgun/agents/tools/web_search/__init__.py +60 -0
  39. shotgun/agents/tools/web_search/anthropic.py +144 -0
  40. shotgun/agents/tools/web_search/gemini.py +85 -0
  41. shotgun/agents/tools/web_search/openai.py +98 -0
  42. shotgun/agents/tools/web_search/utils.py +20 -0
  43. shotgun/build_constants.py +20 -0
  44. shotgun/cli/__init__.py +1 -0
  45. shotgun/cli/codebase/__init__.py +5 -0
  46. shotgun/cli/codebase/commands.py +202 -0
  47. shotgun/cli/codebase/models.py +21 -0
  48. shotgun/cli/config.py +275 -0
  49. shotgun/cli/export.py +81 -0
  50. shotgun/cli/models.py +10 -0
  51. shotgun/cli/plan.py +73 -0
  52. shotgun/cli/research.py +85 -0
  53. shotgun/cli/specify.py +69 -0
  54. shotgun/cli/tasks.py +78 -0
  55. shotgun/cli/update.py +152 -0
  56. shotgun/cli/utils.py +25 -0
  57. shotgun/codebase/__init__.py +12 -0
  58. shotgun/codebase/core/__init__.py +46 -0
  59. shotgun/codebase/core/change_detector.py +358 -0
  60. shotgun/codebase/core/code_retrieval.py +243 -0
  61. shotgun/codebase/core/ingestor.py +1497 -0
  62. shotgun/codebase/core/language_config.py +297 -0
  63. shotgun/codebase/core/manager.py +1662 -0
  64. shotgun/codebase/core/nl_query.py +331 -0
  65. shotgun/codebase/core/parser_loader.py +128 -0
  66. shotgun/codebase/models.py +111 -0
  67. shotgun/codebase/service.py +206 -0
  68. shotgun/logging_config.py +227 -0
  69. shotgun/main.py +167 -0
  70. shotgun/posthog_telemetry.py +158 -0
  71. shotgun/prompts/__init__.py +5 -0
  72. shotgun/prompts/agents/__init__.py +1 -0
  73. shotgun/prompts/agents/export.j2 +350 -0
  74. shotgun/prompts/agents/partials/codebase_understanding.j2 +87 -0
  75. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +37 -0
  76. shotgun/prompts/agents/partials/content_formatting.j2 +65 -0
  77. shotgun/prompts/agents/partials/interactive_mode.j2 +26 -0
  78. shotgun/prompts/agents/plan.j2 +144 -0
  79. shotgun/prompts/agents/research.j2 +69 -0
  80. shotgun/prompts/agents/specify.j2 +51 -0
  81. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +19 -0
  82. shotgun/prompts/agents/state/system_state.j2 +31 -0
  83. shotgun/prompts/agents/tasks.j2 +143 -0
  84. shotgun/prompts/codebase/__init__.py +1 -0
  85. shotgun/prompts/codebase/cypher_query_patterns.j2 +223 -0
  86. shotgun/prompts/codebase/cypher_system.j2 +28 -0
  87. shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
  88. shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
  89. shotgun/prompts/codebase/partials/graph_schema.j2 +30 -0
  90. shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
  91. shotgun/prompts/history/__init__.py +1 -0
  92. shotgun/prompts/history/incremental_summarization.j2 +53 -0
  93. shotgun/prompts/history/summarization.j2 +46 -0
  94. shotgun/prompts/loader.py +140 -0
  95. shotgun/py.typed +0 -0
  96. shotgun/sdk/__init__.py +13 -0
  97. shotgun/sdk/codebase.py +219 -0
  98. shotgun/sdk/exceptions.py +17 -0
  99. shotgun/sdk/models.py +189 -0
  100. shotgun/sdk/services.py +23 -0
  101. shotgun/sentry_telemetry.py +87 -0
  102. shotgun/telemetry.py +93 -0
  103. shotgun/tui/__init__.py +0 -0
  104. shotgun/tui/app.py +116 -0
  105. shotgun/tui/commands/__init__.py +76 -0
  106. shotgun/tui/components/prompt_input.py +69 -0
  107. shotgun/tui/components/spinner.py +86 -0
  108. shotgun/tui/components/splash.py +25 -0
  109. shotgun/tui/components/vertical_tail.py +13 -0
  110. shotgun/tui/screens/chat.py +782 -0
  111. shotgun/tui/screens/chat.tcss +43 -0
  112. shotgun/tui/screens/chat_screen/__init__.py +0 -0
  113. shotgun/tui/screens/chat_screen/command_providers.py +219 -0
  114. shotgun/tui/screens/chat_screen/hint_message.py +40 -0
  115. shotgun/tui/screens/chat_screen/history.py +221 -0
  116. shotgun/tui/screens/directory_setup.py +113 -0
  117. shotgun/tui/screens/provider_config.py +221 -0
  118. shotgun/tui/screens/splash.py +31 -0
  119. shotgun/tui/styles.tcss +10 -0
  120. shotgun/tui/utils/__init__.py +5 -0
  121. shotgun/tui/utils/mode_progress.py +257 -0
  122. shotgun/utils/__init__.py +5 -0
  123. shotgun/utils/env_utils.py +35 -0
  124. shotgun/utils/file_system_utils.py +36 -0
  125. shotgun/utils/update_checker.py +375 -0
  126. shotgun_sh-0.1.0.dist-info/METADATA +466 -0
  127. shotgun_sh-0.1.0.dist-info/RECORD +130 -0
  128. shotgun_sh-0.1.0.dist-info/WHEEL +4 -0
  129. shotgun_sh-0.1.0.dist-info/entry_points.txt +2 -0
  130. shotgun_sh-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -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"