shotgun-sh 0.1.0.dev20__py3-none-any.whl → 0.1.0.dev23__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 (60) hide show
  1. shotgun/agents/agent_manager.py +100 -16
  2. shotgun/agents/common.py +142 -28
  3. shotgun/agents/conversation_history.py +56 -0
  4. shotgun/agents/conversation_manager.py +105 -0
  5. shotgun/agents/export.py +5 -2
  6. shotgun/agents/models.py +21 -7
  7. shotgun/agents/plan.py +2 -1
  8. shotgun/agents/research.py +2 -1
  9. shotgun/agents/specify.py +2 -1
  10. shotgun/agents/tasks.py +5 -2
  11. shotgun/agents/tools/codebase/codebase_shell.py +2 -2
  12. shotgun/agents/tools/codebase/directory_lister.py +1 -1
  13. shotgun/agents/tools/codebase/file_read.py +1 -1
  14. shotgun/agents/tools/codebase/query_graph.py +1 -1
  15. shotgun/agents/tools/codebase/retrieve_code.py +1 -1
  16. shotgun/agents/tools/file_management.py +67 -2
  17. shotgun/main.py +9 -1
  18. shotgun/prompts/agents/export.j2 +14 -11
  19. shotgun/prompts/agents/partials/codebase_understanding.j2 +9 -0
  20. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +6 -9
  21. shotgun/prompts/agents/plan.j2 +9 -13
  22. shotgun/prompts/agents/research.j2 +11 -14
  23. shotgun/prompts/agents/specify.j2 +9 -12
  24. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +5 -1
  25. shotgun/prompts/agents/state/system_state.j2 +27 -5
  26. shotgun/prompts/agents/tasks.j2 +12 -12
  27. shotgun/sdk/models.py +1 -1
  28. shotgun/sdk/services.py +0 -14
  29. shotgun/tui/app.py +9 -4
  30. shotgun/tui/screens/chat.py +92 -30
  31. shotgun/tui/screens/chat_screen/command_providers.py +1 -1
  32. shotgun/tui/screens/chat_screen/history.py +6 -0
  33. shotgun/tui/utils/__init__.py +5 -0
  34. shotgun/tui/utils/mode_progress.py +257 -0
  35. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/METADATA +8 -9
  36. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/RECORD +39 -56
  37. shotgun/agents/artifact_state.py +0 -58
  38. shotgun/agents/tools/artifact_management.py +0 -481
  39. shotgun/artifacts/__init__.py +0 -17
  40. shotgun/artifacts/exceptions.py +0 -89
  41. shotgun/artifacts/manager.py +0 -530
  42. shotgun/artifacts/models.py +0 -334
  43. shotgun/artifacts/service.py +0 -463
  44. shotgun/artifacts/templates/__init__.py +0 -10
  45. shotgun/artifacts/templates/loader.py +0 -252
  46. shotgun/artifacts/templates/models.py +0 -136
  47. shotgun/artifacts/templates/plan/delivery_and_release_plan.yaml +0 -66
  48. shotgun/artifacts/templates/research/market_research.yaml +0 -585
  49. shotgun/artifacts/templates/research/sdk_comparison.yaml +0 -257
  50. shotgun/artifacts/templates/specify/prd.yaml +0 -331
  51. shotgun/artifacts/templates/specify/product_spec.yaml +0 -301
  52. shotgun/artifacts/utils.py +0 -76
  53. shotgun/prompts/agents/partials/artifact_system.j2 +0 -32
  54. shotgun/prompts/agents/state/artifact_templates_available.j2 +0 -20
  55. shotgun/prompts/agents/state/existing_artifacts_available.j2 +0 -25
  56. shotgun/sdk/artifact_models.py +0 -186
  57. shotgun/sdk/artifacts.py +0 -448
  58. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/WHEEL +0 -0
  59. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/entry_points.txt +0 -0
  60. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,12 @@
1
1
  """Agent manager for coordinating multiple AI agents with shared message history."""
2
2
 
3
3
  import logging
4
- from collections.abc import AsyncIterable
5
- from dataclasses import dataclass, field
6
- from enum import Enum
7
- from typing import Any, cast
4
+ from collections.abc import AsyncIterable, Sequence
5
+ from dataclasses import dataclass, field, is_dataclass, replace
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ if TYPE_CHECKING:
9
+ from shotgun.agents.conversation_history import ConversationState
8
10
 
9
11
  from pydantic_ai import (
10
12
  Agent,
@@ -21,6 +23,7 @@ from pydantic_ai.messages import (
21
23
  FunctionToolResultEvent,
22
24
  ModelMessage,
23
25
  ModelRequest,
26
+ ModelRequestPart,
24
27
  ModelResponse,
25
28
  ModelResponsePart,
26
29
  PartDeltaEvent,
@@ -32,11 +35,12 @@ from pydantic_ai.messages import (
32
35
  from textual.message import Message
33
36
  from textual.widget import Widget
34
37
 
35
- from shotgun.agents.common import add_system_prompt_message
38
+ from shotgun.agents.common import add_system_prompt_message, add_system_status_message
39
+ from shotgun.agents.models import AgentType, FileOperation
36
40
 
37
41
  from .export import create_export_agent
38
42
  from .history.compaction import apply_persistent_compaction
39
- from .models import AgentDeps, AgentRuntimeOptions, FileOperation
43
+ from .models import AgentDeps, AgentRuntimeOptions
40
44
  from .plan import create_plan_agent
41
45
  from .research import create_research_agent
42
46
  from .specify import create_specify_agent
@@ -45,16 +49,6 @@ from .tasks import create_tasks_agent
45
49
  logger = logging.getLogger(__name__)
46
50
 
47
51
 
48
- class AgentType(Enum):
49
- """Enumeration for available agent types (for Python < 3.11)."""
50
-
51
- RESEARCH = "research"
52
- PLAN = "plan"
53
- TASKS = "tasks"
54
- SPECIFY = "specify"
55
- EXPORT = "export"
56
-
57
-
58
52
  class MessageHistoryUpdated(Message):
59
53
  """Event posted when the message history is updated."""
60
54
 
@@ -122,6 +116,7 @@ class AgentManager(Widget):
122
116
  agent_runtime_options = AgentRuntimeOptions(
123
117
  interactive_mode=self.deps.interactive_mode,
124
118
  working_directory=self.deps.working_directory,
119
+ is_tui_context=self.deps.is_tui_context,
125
120
  max_iterations=self.deps.max_iterations,
126
121
  queue=self.deps.queue,
127
122
  tasks=self.deps.tasks,
@@ -280,6 +275,11 @@ class AgentManager(Widget):
280
275
  # Start with persistent message history
281
276
  message_history = self.message_history
282
277
 
278
+ deps.agent_mode = self._current_agent_type
279
+
280
+ # Add a system status message so the agent knows whats going on
281
+ message_history = await add_system_status_message(deps, message_history)
282
+
283
283
  # Check if the message history already has a system prompt
284
284
  has_system_prompt = any(
285
285
  hasattr(msg, "parts")
@@ -459,3 +459,87 @@ class AgentManager(Widget):
459
459
  file_operations=file_operations,
460
460
  )
461
461
  )
462
+
463
+ def _filter_system_prompts(
464
+ self, messages: list[ModelMessage]
465
+ ) -> list[ModelMessage]:
466
+ """Filter out system prompts from messages for UI display.
467
+
468
+ Args:
469
+ messages: List of messages that may contain system prompts
470
+
471
+ Returns:
472
+ List of messages without system prompt parts
473
+ """
474
+ from pydantic_ai.messages import SystemPromptPart
475
+
476
+ filtered_messages: list[ModelMessage] = []
477
+ for msg in messages:
478
+ parts: Sequence[ModelRequestPart] | Sequence[ModelResponsePart] | None = (
479
+ msg.parts if hasattr(msg, "parts") else None
480
+ )
481
+ if not parts:
482
+ filtered_messages.append(msg)
483
+ continue
484
+
485
+ non_system_parts = [
486
+ part for part in parts if not isinstance(part, SystemPromptPart)
487
+ ]
488
+
489
+ if not non_system_parts:
490
+ # Skip messages made up entirely of system prompt parts (e.g. system message)
491
+ continue
492
+
493
+ if len(non_system_parts) == len(parts):
494
+ # Nothing was filtered – keep original message
495
+ filtered_messages.append(msg)
496
+ continue
497
+
498
+ if is_dataclass(msg):
499
+ filtered_messages.append(
500
+ # ignore types because of the convoluted Request | Response types
501
+ replace(msg, parts=cast(Any, non_system_parts))
502
+ )
503
+ else:
504
+ filtered_messages.append(msg)
505
+ return filtered_messages
506
+
507
+ def get_conversation_state(self) -> "ConversationState":
508
+ """Get the current conversation state.
509
+
510
+ Returns:
511
+ ConversationState object containing UI and agent messages and current type
512
+ """
513
+ from shotgun.agents.conversation_history import ConversationState
514
+
515
+ return ConversationState(
516
+ agent_messages=self.message_history.copy(),
517
+ agent_type=self._current_agent_type.value,
518
+ )
519
+
520
+ def restore_conversation_state(self, state: "ConversationState") -> None:
521
+ """Restore conversation state from a saved state.
522
+
523
+ Args:
524
+ state: ConversationState object to restore
525
+ """
526
+ # Restore message history for agents (includes system prompts)
527
+ self.message_history = state.agent_messages.copy()
528
+
529
+ # Filter out system prompts for UI display
530
+ self.ui_message_history = self._filter_system_prompts(state.agent_messages)
531
+
532
+ # Restore agent type
533
+ self._current_agent_type = AgentType(state.agent_type)
534
+
535
+ # Notify listeners about the restored messages
536
+ self._post_messages_updated()
537
+
538
+
539
+ # Re-export AgentType for backward compatibility
540
+ __all__ = [
541
+ "AgentManager",
542
+ "AgentType",
543
+ "MessageHistoryUpdated",
544
+ "PartialResponseMessage",
545
+ ]
shotgun/agents/common.py CHANGED
@@ -15,16 +15,16 @@ from pydantic_ai.agent import AgentRunResult
15
15
  from pydantic_ai.messages import (
16
16
  ModelMessage,
17
17
  ModelRequest,
18
- ModelResponse,
19
18
  SystemPromptPart,
20
- TextPart,
21
19
  )
22
20
 
23
21
  from shotgun.agents.config import ProviderType, get_config_manager, get_provider_model
22
+ from shotgun.agents.models import AgentType
24
23
  from shotgun.logging_config import get_logger
25
24
  from shotgun.prompts import PromptLoader
26
- from shotgun.sdk.services import get_artifact_service, get_codebase_service
25
+ from shotgun.sdk.services import get_codebase_service
27
26
  from shotgun.utils import ensure_shotgun_directory_exists
27
+ from shotgun.utils.file_system_utils import get_shotgun_base_path
28
28
 
29
29
  from .history import token_limit_compactor
30
30
  from .history.compaction import apply_persistent_compaction
@@ -40,14 +40,7 @@ from .tools import (
40
40
  retrieve_code,
41
41
  write_file,
42
42
  )
43
- from .tools.artifact_management import (
44
- create_artifact,
45
- list_artifact_templates,
46
- list_artifacts,
47
- read_artifact,
48
- read_artifact_section,
49
- write_artifact_section,
50
- )
43
+ from .tools.file_management import AGENT_DIRECTORIES
51
44
 
52
45
  logger = get_logger(__name__)
53
46
 
@@ -71,21 +64,24 @@ async def add_system_status_message(
71
64
  message_history = message_history or []
72
65
  codebase_understanding_graphs = await deps.codebase_service.list_graphs()
73
66
 
74
- # Collect artifact state information
75
- from .artifact_state import collect_artifact_state
67
+ # Get existing files for the agent
68
+ existing_files = get_agent_existing_files(deps.agent_mode)
76
69
 
77
- artifact_state = collect_artifact_state()
70
+ # Extract table of contents from the agent's markdown file
71
+ markdown_toc = extract_markdown_toc(deps.agent_mode)
78
72
 
79
73
  system_state = prompt_loader.render(
80
74
  "agents/state/system_state.j2",
81
75
  codebase_understanding_graphs=codebase_understanding_graphs,
82
- **artifact_state,
76
+ is_tui_context=deps.is_tui_context,
77
+ existing_files=existing_files,
78
+ markdown_toc=markdown_toc,
83
79
  )
84
80
 
85
81
  message_history.append(
86
- ModelResponse(
82
+ ModelRequest(
87
83
  parts=[
88
- TextPart(content=system_state),
84
+ SystemPromptPart(content=system_state),
89
85
  ]
90
86
  )
91
87
  )
@@ -98,14 +94,17 @@ def create_base_agent(
98
94
  load_codebase_understanding_tools: bool = True,
99
95
  additional_tools: list[Any] | None = None,
100
96
  provider: ProviderType | None = None,
97
+ agent_mode: AgentType | None = None,
101
98
  ) -> tuple[Agent[AgentDeps, str | DeferredToolRequests], AgentDeps]:
102
99
  """Create a base agent with common configuration.
103
100
 
104
101
  Args:
105
102
  system_prompt_fn: Function that will be decorated as system_prompt
106
103
  agent_runtime_options: Agent runtime options for the agent
104
+ load_codebase_understanding_tools: Whether to load codebase understanding tools
107
105
  additional_tools: Optional list of additional tools
108
106
  provider: Optional provider override. If None, uses configured default
107
+ agent_mode: The mode of the agent (research, plan, tasks, specify, export)
109
108
 
110
109
  Returns:
111
110
  Tuple of (Configured Pydantic AI agent, Agent dependencies)
@@ -127,13 +126,12 @@ def create_base_agent(
127
126
 
128
127
  # Create deps with model config and services
129
128
  codebase_service = get_codebase_service()
130
- artifact_service = get_artifact_service()
131
129
  deps = AgentDeps(
132
130
  **agent_runtime_options.model_dump(),
133
131
  llm_model=model_config,
134
132
  codebase_service=codebase_service,
135
- artifact_service=artifact_service,
136
133
  system_prompt_fn=system_prompt_fn,
134
+ agent_mode=agent_mode,
137
135
  )
138
136
 
139
137
  except Exception as e:
@@ -181,14 +179,6 @@ def create_base_agent(
181
179
  agent.tool(append_file)
182
180
  agent.tool(read_file)
183
181
 
184
- # Register artifact management tools (always available)
185
- agent.tool(create_artifact)
186
- agent.tool(list_artifacts)
187
- agent.tool(list_artifact_templates)
188
- agent.tool(read_artifact)
189
- agent.tool(read_artifact_section)
190
- agent.tool(write_artifact_section)
191
-
192
182
  # Register codebase understanding tools (conditional)
193
183
  if load_codebase_understanding_tools:
194
184
  agent.tool(query_graph)
@@ -200,10 +190,134 @@ def create_base_agent(
200
190
  else:
201
191
  logger.debug("🚫🧠 Codebase understanding tools not registered")
202
192
 
203
- logger.debug("✅ Agent creation complete with artifact and codebase tools")
193
+ logger.debug("✅ Agent creation complete with codebase tools")
204
194
  return agent, deps
205
195
 
206
196
 
197
+ def extract_markdown_toc(agent_mode: AgentType | None) -> str | None:
198
+ """Extract table of contents from agent's markdown file.
199
+
200
+ Args:
201
+ agent_mode: The agent mode to extract TOC for
202
+
203
+ Returns:
204
+ Formatted TOC string (up to 2000 chars) or None if not applicable
205
+ """
206
+ # Skip for EXPORT mode or no mode
207
+ if (
208
+ not agent_mode
209
+ or agent_mode == AgentType.EXPORT
210
+ or agent_mode not in AGENT_DIRECTORIES
211
+ ):
212
+ return None
213
+
214
+ base_path = get_shotgun_base_path()
215
+ md_file = AGENT_DIRECTORIES[agent_mode]
216
+ md_path = base_path / md_file
217
+
218
+ # Check if the markdown file exists
219
+ if not md_path.exists():
220
+ return None
221
+
222
+ try:
223
+ content = md_path.read_text(encoding="utf-8")
224
+ lines = content.split("\n")
225
+
226
+ # Extract headings
227
+ toc_lines = []
228
+ for line in lines:
229
+ stripped = line.strip()
230
+ if stripped.startswith("#"):
231
+ # Count the heading level
232
+ level = 0
233
+ for char in stripped:
234
+ if char == "#":
235
+ level += 1
236
+ else:
237
+ break
238
+
239
+ # Get the heading text (remove the # symbols and clean up)
240
+ heading_text = stripped[level:].strip()
241
+ if heading_text:
242
+ # Add indentation based on level
243
+ indent = " " * (level - 1)
244
+ toc_lines.append(f"{indent}{'#' * level} {heading_text}")
245
+
246
+ if not toc_lines:
247
+ return None
248
+
249
+ # Join and truncate to 2000 characters
250
+ toc = "\n".join(toc_lines)
251
+ if len(toc) > 2000:
252
+ toc = toc[:1997] + "..."
253
+
254
+ return toc
255
+
256
+ except Exception as e:
257
+ logger.debug(f"Failed to extract TOC from {md_file}: {e}")
258
+ return None
259
+
260
+
261
+ def get_agent_existing_files(agent_mode: AgentType | None = None) -> list[str]:
262
+ """Get list of existing files for the given agent mode.
263
+
264
+ Args:
265
+ agent_mode: The agent mode to check files for. If None, lists all files.
266
+
267
+ Returns:
268
+ List of existing file paths relative to .shotgun directory
269
+ """
270
+ base_path = get_shotgun_base_path()
271
+ existing_files = []
272
+
273
+ # If no agent mode, list all files in base path and first level subdirectories
274
+ if agent_mode is None:
275
+ # List files in the root .shotgun directory
276
+ for item in base_path.iterdir():
277
+ if item.is_file():
278
+ existing_files.append(item.name)
279
+ elif item.is_dir():
280
+ # List files in first-level subdirectories
281
+ for subitem in item.iterdir():
282
+ if subitem.is_file():
283
+ relative_path = subitem.relative_to(base_path)
284
+ existing_files.append(str(relative_path))
285
+ return existing_files
286
+
287
+ # Handle specific agent modes
288
+ if agent_mode not in AGENT_DIRECTORIES:
289
+ return []
290
+
291
+ if agent_mode == AgentType.EXPORT:
292
+ # For export agent, list all files in exports directory
293
+ exports_dir = base_path / "exports"
294
+ if exports_dir.exists():
295
+ for file_path in exports_dir.rglob("*"):
296
+ if file_path.is_file():
297
+ relative_path = file_path.relative_to(base_path)
298
+ existing_files.append(str(relative_path))
299
+ else:
300
+ # For other agents, check both .md file and directory with same name
301
+ allowed_file = AGENT_DIRECTORIES[agent_mode]
302
+
303
+ # Check for the .md file
304
+ md_file_path = base_path / allowed_file
305
+ if md_file_path.exists():
306
+ existing_files.append(allowed_file)
307
+
308
+ # Check for directory with same base name (e.g., research/ for research.md)
309
+ base_name = allowed_file.replace(".md", "")
310
+ dir_path = base_path / base_name
311
+ if dir_path.exists() and dir_path.is_dir():
312
+ # List all files in the directory
313
+ for file_path in dir_path.rglob("*"):
314
+ if file_path.is_file():
315
+ relative_path = file_path.relative_to(base_path)
316
+ existing_files.append(str(relative_path))
317
+
318
+ return existing_files
319
+
320
+
207
321
  def build_agent_system_prompt(
208
322
  agent_type: str,
209
323
  ctx: RunContext[AgentDeps],
@@ -0,0 +1,56 @@
1
+ """Models and utilities for persisting TUI conversation history."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+ from pydantic_ai.messages import (
8
+ ModelMessage,
9
+ ModelMessagesTypeAdapter,
10
+ )
11
+ from pydantic_core import to_jsonable_python
12
+
13
+
14
+ class ConversationState(BaseModel):
15
+ """Represents the complete state of a conversation in memory."""
16
+
17
+ agent_messages: list[ModelMessage]
18
+ agent_type: str # Will store AgentType.value
19
+
20
+ model_config = ConfigDict(arbitrary_types_allowed=True)
21
+
22
+
23
+ class ConversationHistory(BaseModel):
24
+ """Persistent conversation history for TUI sessions."""
25
+
26
+ version: int = 1
27
+ agent_history: list[dict[str, Any]] = Field(
28
+ default_factory=list
29
+ ) # Will store serialized ModelMessage objects
30
+ last_agent_model: str = "research"
31
+ updated_at: datetime = Field(default_factory=datetime.now)
32
+
33
+ model_config = ConfigDict(arbitrary_types_allowed=True)
34
+
35
+ def set_agent_messages(self, messages: list[ModelMessage]) -> None:
36
+ """Set agent_history from a list of ModelMessage objects.
37
+
38
+ Args:
39
+ messages: List of ModelMessage objects to serialize and store
40
+ """
41
+ # Serialize ModelMessage list to JSON-serializable format
42
+ self.agent_history = to_jsonable_python(
43
+ messages, fallback=lambda x: str(x), exclude_none=True
44
+ )
45
+
46
+ def get_agent_messages(self) -> list[ModelMessage]:
47
+ """Get agent_history as a list of ModelMessage objects.
48
+
49
+ Returns:
50
+ List of deserialized ModelMessage objects
51
+ """
52
+ if not self.agent_history:
53
+ return []
54
+
55
+ # Deserialize from JSON format back to ModelMessage objects
56
+ return ModelMessagesTypeAdapter.validate_python(self.agent_history)
@@ -0,0 +1,105 @@
1
+ """Manager for handling conversation persistence operations."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from shotgun.logging_config import get_logger
7
+ from shotgun.utils import get_shotgun_home
8
+
9
+ from .conversation_history import ConversationHistory
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ class ConversationManager:
15
+ """Handles saving and loading conversation history."""
16
+
17
+ def __init__(self, conversation_path: Path | None = None):
18
+ """Initialize ConversationManager.
19
+
20
+ Args:
21
+ conversation_path: Path to conversation file.
22
+ If None, uses default ~/.shotgun-sh/conversation.json
23
+ """
24
+ if conversation_path is None:
25
+ self.conversation_path = get_shotgun_home() / "conversation.json"
26
+ else:
27
+ self.conversation_path = conversation_path
28
+
29
+ def save(self, conversation: ConversationHistory) -> None:
30
+ """Save conversation history to file.
31
+
32
+ Args:
33
+ conversation: ConversationHistory to save
34
+ """
35
+ # Ensure directory exists
36
+ self.conversation_path.parent.mkdir(parents=True, exist_ok=True)
37
+
38
+ try:
39
+ # Update timestamp
40
+ from datetime import datetime
41
+
42
+ conversation.updated_at = datetime.now()
43
+
44
+ # Serialize to JSON using Pydantic's model_dump
45
+ data = conversation.model_dump(mode="json")
46
+
47
+ with open(self.conversation_path, "w", encoding="utf-8") as f:
48
+ json.dump(data, f, indent=2, ensure_ascii=False)
49
+
50
+ logger.debug("Conversation saved to %s", self.conversation_path)
51
+
52
+ except Exception as e:
53
+ logger.error(
54
+ "Failed to save conversation to %s: %s", self.conversation_path, e
55
+ )
56
+ # Don't raise - we don't want to interrupt the user's session
57
+
58
+ def load(self) -> ConversationHistory | None:
59
+ """Load conversation history from file.
60
+
61
+ Returns:
62
+ ConversationHistory if file exists and is valid, None otherwise
63
+ """
64
+ if not self.conversation_path.exists():
65
+ logger.debug("No conversation history found at %s", self.conversation_path)
66
+ return None
67
+
68
+ try:
69
+ with open(self.conversation_path, encoding="utf-8") as f:
70
+ data = json.load(f)
71
+
72
+ conversation = ConversationHistory.model_validate(data)
73
+ logger.debug(
74
+ "Conversation loaded from %s with %d agent messages",
75
+ self.conversation_path,
76
+ len(conversation.agent_history),
77
+ )
78
+ return conversation
79
+
80
+ except Exception as e:
81
+ logger.error(
82
+ "Failed to load conversation from %s: %s", self.conversation_path, e
83
+ )
84
+ return None
85
+
86
+ def clear(self) -> None:
87
+ """Delete the conversation history file."""
88
+ if self.conversation_path.exists():
89
+ try:
90
+ self.conversation_path.unlink()
91
+ logger.debug(
92
+ "Conversation history cleared at %s", self.conversation_path
93
+ )
94
+ except Exception as e:
95
+ logger.error(
96
+ "Failed to clear conversation at %s: %s", self.conversation_path, e
97
+ )
98
+
99
+ def exists(self) -> bool:
100
+ """Check if a conversation history file exists.
101
+
102
+ Returns:
103
+ True if conversation file exists, False otherwise
104
+ """
105
+ return self.conversation_path.exists()
shotgun/agents/export.py CHANGED
@@ -19,7 +19,7 @@ from .common import (
19
19
  create_usage_limits,
20
20
  run_agent,
21
21
  )
22
- from .models import AgentDeps, AgentRuntimeOptions
22
+ from .models import AgentDeps, AgentRuntimeOptions, AgentType
23
23
 
24
24
  logger = get_logger(__name__)
25
25
 
@@ -41,7 +41,10 @@ def create_export_agent(
41
41
  system_prompt_fn = partial(build_agent_system_prompt, "export")
42
42
 
43
43
  agent, deps = create_base_agent(
44
- system_prompt_fn, agent_runtime_options, provider=provider
44
+ system_prompt_fn,
45
+ agent_runtime_options,
46
+ provider=provider,
47
+ agent_mode=AgentType.EXPORT,
45
48
  )
46
49
  return agent, deps
47
50
 
shotgun/agents/models.py CHANGED
@@ -4,7 +4,7 @@ import os
4
4
  from asyncio import Future, Queue
5
5
  from collections.abc import Callable
6
6
  from datetime import datetime
7
- from enum import Enum
7
+ from enum import Enum, StrEnum
8
8
  from pathlib import Path
9
9
  from typing import TYPE_CHECKING
10
10
 
@@ -14,10 +14,19 @@ from pydantic_ai import RunContext
14
14
  from .config.models import ModelConfig
15
15
 
16
16
  if TYPE_CHECKING:
17
- from shotgun.artifacts.service import ArtifactService
18
17
  from shotgun.codebase.service import CodebaseService
19
18
 
20
19
 
20
+ class AgentType(StrEnum):
21
+ """Enumeration for available agent types."""
22
+
23
+ RESEARCH = "research"
24
+ SPECIFY = "specify"
25
+ PLAN = "plan"
26
+ TASKS = "tasks"
27
+ EXPORT = "export"
28
+
29
+
21
30
  class UserAnswer(BaseModel):
22
31
  """A answer from the user."""
23
32
 
@@ -60,6 +69,11 @@ class AgentRuntimeOptions(BaseModel):
60
69
  description="Working directory for agent operations",
61
70
  )
62
71
 
72
+ is_tui_context: bool = Field(
73
+ default=False,
74
+ description="Whether the agent is running in TUI context",
75
+ )
76
+
63
77
  max_iterations: int = Field(
64
78
  default=10,
65
79
  ge=1,
@@ -219,10 +233,6 @@ class AgentDeps(AgentRuntimeOptions):
219
233
  description="Codebase service for code analysis tools",
220
234
  )
221
235
 
222
- artifact_service: "ArtifactService" = Field(
223
- description="Artifact service for managing structured artifacts",
224
- )
225
-
226
236
  system_prompt_fn: Callable[[RunContext["AgentDeps"]], str] = Field(
227
237
  description="Function that generates the system prompt for this agent",
228
238
  )
@@ -232,10 +242,14 @@ class AgentDeps(AgentRuntimeOptions):
232
242
  description="Tracker for file operations during agent run",
233
243
  )
234
244
 
245
+ agent_mode: AgentType | None = Field(
246
+ default=None,
247
+ description="Current agent mode for file scoping",
248
+ )
249
+
235
250
 
236
251
  # Rebuild model to resolve forward references after imports are available
237
252
  try:
238
- from shotgun.artifacts.service import ArtifactService
239
253
  from shotgun.codebase.service import CodebaseService
240
254
 
241
255
  AgentDeps.model_rebuild()
shotgun/agents/plan.py CHANGED
@@ -19,7 +19,7 @@ from .common import (
19
19
  create_usage_limits,
20
20
  run_agent,
21
21
  )
22
- from .models import AgentDeps, AgentRuntimeOptions
22
+ from .models import AgentDeps, AgentRuntimeOptions, AgentType
23
23
 
24
24
  logger = get_logger(__name__)
25
25
 
@@ -46,6 +46,7 @@ def create_plan_agent(
46
46
  load_codebase_understanding_tools=True,
47
47
  additional_tools=None,
48
48
  provider=provider,
49
+ agent_mode=AgentType.PLAN,
49
50
  )
50
51
  return agent, deps
51
52
 
@@ -21,7 +21,7 @@ from .common import (
21
21
  create_usage_limits,
22
22
  run_agent,
23
23
  )
24
- from .models import AgentDeps, AgentRuntimeOptions
24
+ from .models import AgentDeps, AgentRuntimeOptions, AgentType
25
25
  from .tools import get_available_web_search_tools
26
26
 
27
27
  logger = get_logger(__name__)
@@ -60,6 +60,7 @@ def create_research_agent(
60
60
  load_codebase_understanding_tools=True,
61
61
  additional_tools=web_search_tools,
62
62
  provider=provider,
63
+ agent_mode=AgentType.RESEARCH,
63
64
  )
64
65
  return agent, deps
65
66