shotgun-sh 0.1.0.dev12__py3-none-any.whl → 0.1.0.dev14__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 (73) hide show
  1. shotgun/agents/agent_manager.py +16 -3
  2. shotgun/agents/artifact_state.py +58 -0
  3. shotgun/agents/common.py +137 -88
  4. shotgun/agents/config/constants.py +18 -0
  5. shotgun/agents/config/manager.py +68 -16
  6. shotgun/agents/config/models.py +61 -0
  7. shotgun/agents/config/provider.py +11 -6
  8. shotgun/agents/history/compaction.py +85 -0
  9. shotgun/agents/history/constants.py +19 -0
  10. shotgun/agents/history/context_extraction.py +108 -0
  11. shotgun/agents/history/history_building.py +104 -0
  12. shotgun/agents/history/history_processors.py +354 -157
  13. shotgun/agents/history/message_utils.py +46 -0
  14. shotgun/agents/history/token_counting.py +429 -0
  15. shotgun/agents/history/token_estimation.py +138 -0
  16. shotgun/agents/models.py +131 -1
  17. shotgun/agents/plan.py +15 -37
  18. shotgun/agents/research.py +10 -45
  19. shotgun/agents/specify.py +97 -0
  20. shotgun/agents/tasks.py +7 -36
  21. shotgun/agents/tools/artifact_management.py +482 -0
  22. shotgun/agents/tools/file_management.py +31 -12
  23. shotgun/agents/tools/web_search/anthropic.py +78 -17
  24. shotgun/agents/tools/web_search/gemini.py +1 -1
  25. shotgun/agents/tools/web_search/openai.py +16 -2
  26. shotgun/artifacts/__init__.py +17 -0
  27. shotgun/artifacts/exceptions.py +89 -0
  28. shotgun/artifacts/manager.py +530 -0
  29. shotgun/artifacts/models.py +334 -0
  30. shotgun/artifacts/service.py +463 -0
  31. shotgun/artifacts/templates/__init__.py +10 -0
  32. shotgun/artifacts/templates/loader.py +252 -0
  33. shotgun/artifacts/templates/models.py +136 -0
  34. shotgun/artifacts/templates/plan/delivery_and_release_plan.yaml +66 -0
  35. shotgun/artifacts/templates/research/market_research.yaml +585 -0
  36. shotgun/artifacts/templates/research/sdk_comparison.yaml +257 -0
  37. shotgun/artifacts/templates/specify/prd.yaml +331 -0
  38. shotgun/artifacts/templates/specify/product_spec.yaml +301 -0
  39. shotgun/artifacts/utils.py +76 -0
  40. shotgun/cli/plan.py +1 -4
  41. shotgun/cli/specify.py +69 -0
  42. shotgun/cli/tasks.py +0 -4
  43. shotgun/codebase/core/nl_query.py +4 -4
  44. shotgun/logging_config.py +23 -7
  45. shotgun/main.py +7 -6
  46. shotgun/prompts/agents/partials/artifact_system.j2 +35 -0
  47. shotgun/prompts/agents/partials/codebase_understanding.j2 +1 -2
  48. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
  49. shotgun/prompts/agents/partials/content_formatting.j2 +65 -0
  50. shotgun/prompts/agents/partials/interactive_mode.j2 +10 -2
  51. shotgun/prompts/agents/plan.j2 +33 -32
  52. shotgun/prompts/agents/research.j2 +39 -29
  53. shotgun/prompts/agents/specify.j2 +32 -0
  54. shotgun/prompts/agents/state/artifact_templates_available.j2 +18 -0
  55. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +3 -1
  56. shotgun/prompts/agents/state/existing_artifacts_available.j2 +23 -0
  57. shotgun/prompts/agents/state/system_state.j2 +9 -1
  58. shotgun/prompts/agents/tasks.j2 +27 -12
  59. shotgun/prompts/history/incremental_summarization.j2 +53 -0
  60. shotgun/sdk/artifact_models.py +186 -0
  61. shotgun/sdk/artifacts.py +448 -0
  62. shotgun/sdk/services.py +14 -0
  63. shotgun/tui/app.py +26 -7
  64. shotgun/tui/screens/chat.py +32 -5
  65. shotgun/tui/screens/directory_setup.py +113 -0
  66. shotgun/utils/file_system_utils.py +6 -1
  67. {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/METADATA +3 -2
  68. shotgun_sh-0.1.0.dev14.dist-info/RECORD +138 -0
  69. shotgun/prompts/user/research.j2 +0 -5
  70. shotgun_sh-0.1.0.dev12.dist-info/RECORD +0 -104
  71. {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/WHEEL +0 -0
  72. {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/entry_points.txt +0 -0
  73. {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev14.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,482 @@
1
+ """Artifact management tools for Pydantic AI agents.
2
+
3
+ These tools provide agents with the ability to create and manage structured
4
+ artifacts instead of flat markdown files.
5
+ """
6
+
7
+ from pydantic_ai import RunContext
8
+
9
+ from shotgun.agents.models import AgentDeps
10
+ from shotgun.artifacts.utils import handle_agent_mode_parsing
11
+ from shotgun.logging_config import setup_logger
12
+
13
+ logger = setup_logger(__name__)
14
+
15
+
16
+ async def create_artifact(
17
+ ctx: RunContext[AgentDeps],
18
+ artifact_id: str,
19
+ agent_mode: str,
20
+ name: str,
21
+ template_id: str = "",
22
+ ) -> str:
23
+ """Create a new artifact.
24
+
25
+ Args:
26
+ ctx: RunContext containing AgentDeps with artifact service
27
+ artifact_id: Unique identifier for the artifact (slug format)
28
+ agent_mode: Agent mode (research, plan, tasks)
29
+ name: Human-readable name for the artifact
30
+ template_id: Optional template ID to use for creating the artifact
31
+
32
+ Returns:
33
+ Success message including template content if template was used, or error message
34
+
35
+ Example:
36
+ create_artifact("market-analysis", "research", "Market Analysis")
37
+ create_artifact("market-study", "research", "Market Study", "research/market_research")
38
+ """
39
+ logger.debug("🔧 Creating artifact: %s/%s", agent_mode, artifact_id)
40
+
41
+ # Parse and validate agent mode
42
+ mode, error_msg = handle_agent_mode_parsing(agent_mode)
43
+ if error_msg:
44
+ logger.error("❌ Create artifact failed: %s", error_msg)
45
+ return f"Error: {error_msg}"
46
+ # Type checker hint: mode is validated above
47
+ if mode is None:
48
+ return "Error: Invalid agent mode"
49
+
50
+ try:
51
+ service = ctx.deps.artifact_service
52
+
53
+ # Pass template_id if provided and not empty
54
+ template_to_use = template_id.strip() if template_id.strip() else None
55
+ artifact = service.create_artifact(artifact_id, mode, name, template_to_use)
56
+
57
+ # Track the artifact file creation
58
+ from shotgun.agents.models import FileOperationType
59
+ from shotgun.utils.file_system_utils import get_shotgun_base_path
60
+
61
+ artifact_path = (
62
+ get_shotgun_base_path() / mode.value / artifact_id / "artifact.yaml"
63
+ )
64
+ ctx.deps.file_tracker.add_operation(artifact_path, FileOperationType.CREATED)
65
+
66
+ success_msg = (
67
+ f"Created artifact '{artifact_id}' in {agent_mode} mode with name '{name}'"
68
+ )
69
+
70
+ # If template was used, include template content in the response
71
+ if artifact.has_template():
72
+ template_content = artifact.load_template_from_file()
73
+ if template_content:
74
+ success_msg += f"\n\nUsing template: {template_content.get('name', artifact.get_template_id())}"
75
+
76
+ if "purpose" in template_content:
77
+ success_msg += f"\nPurpose: {template_content['purpose']}"
78
+ if "prompt" in template_content:
79
+ success_msg += f"\nPrompt: {template_content['prompt']}"
80
+
81
+ if "sections" in template_content and isinstance(
82
+ template_content["sections"], dict
83
+ ):
84
+ success_msg += "\nSections to complete:"
85
+
86
+ # Sort sections by order if available
87
+ sections_dict = template_content["sections"]
88
+ sorted_sections = sorted(
89
+ sections_dict.items(),
90
+ key=lambda x: x[1].get("order", 999)
91
+ if isinstance(x[1], dict)
92
+ else 999,
93
+ )
94
+
95
+ for section_key, section_info in sorted_sections:
96
+ if isinstance(section_info, dict):
97
+ instructions = section_info.get("instructions", "")
98
+ success_msg += f"\n- {section_key}: {instructions}"
99
+ if section_info.get("depends_on"):
100
+ depends_on = section_info["depends_on"]
101
+ if isinstance(depends_on, list):
102
+ success_msg += (
103
+ f" (depends on: {', '.join(depends_on)})"
104
+ )
105
+
106
+ logger.debug("✅ %s", success_msg)
107
+ return success_msg
108
+
109
+ except Exception as e:
110
+ error_msg = f"Failed to create artifact '{artifact_id}': {str(e)}"
111
+ logger.error("❌ Create artifact failed: %s", error_msg)
112
+ return f"Error: {error_msg}"
113
+
114
+
115
+ async def read_artifact(
116
+ ctx: RunContext[AgentDeps],
117
+ artifact_id: str,
118
+ agent_mode: str,
119
+ ) -> str:
120
+ """Read all sections of an artifact.
121
+
122
+ Args:
123
+ ctx: RunContext containing AgentDeps with artifact service
124
+ artifact_id: Artifact identifier
125
+ agent_mode: Agent mode (research, plan, tasks)
126
+
127
+ Returns:
128
+ Combined content of all sections or error message
129
+
130
+ Example:
131
+ read_artifact("market-analysis", "research")
132
+ """
133
+ logger.debug("🔧 Reading artifact: %s/%s", agent_mode, artifact_id)
134
+
135
+ # Parse and validate agent mode
136
+ mode, error_msg = handle_agent_mode_parsing(agent_mode)
137
+ if error_msg:
138
+ logger.error("❌ Read artifact failed: %s", error_msg)
139
+ return f"Error: {error_msg}"
140
+ # Type checker hint: mode is validated above
141
+ if mode is None:
142
+ return "Error: Invalid agent mode"
143
+
144
+ try:
145
+ service = ctx.deps.artifact_service
146
+ artifact = service.get_artifact(artifact_id, mode, "")
147
+
148
+ if not artifact.sections:
149
+ return f"Artifact '{artifact_id}' exists but has no sections."
150
+
151
+ # Combine all sections with headers
152
+ content_parts = [f"# {artifact.name}\n"]
153
+
154
+ # Include template information if artifact was created from a template
155
+ if artifact.has_template():
156
+ template_content = artifact.load_template_from_file()
157
+ if template_content:
158
+ content_parts.append("\n## Template Information\n")
159
+ content_parts.append(f"**Template ID:** {artifact.get_template_id()}\n")
160
+
161
+ # Extract template info from the loaded template file
162
+ if "name" in template_content:
163
+ content_parts.append(f"**Template:** {template_content['name']}\n")
164
+ if "purpose" in template_content:
165
+ content_parts.append(
166
+ f"**Purpose:** {template_content['purpose']}\n"
167
+ )
168
+ if "prompt" in template_content:
169
+ content_parts.append(f"**Prompt:** {template_content['prompt']}\n")
170
+
171
+ if "sections" in template_content and isinstance(
172
+ template_content["sections"], dict
173
+ ):
174
+ content_parts.append("\n### Template Sections:\n")
175
+
176
+ # Sort sections by order if available
177
+ sections_dict = template_content["sections"]
178
+ sorted_sections = sorted(
179
+ sections_dict.items(),
180
+ key=lambda x: x[1].get("order", 999)
181
+ if isinstance(x[1], dict)
182
+ else 999,
183
+ )
184
+
185
+ for section_key, section_info in sorted_sections:
186
+ if isinstance(section_info, dict):
187
+ content_parts.append(
188
+ f"- **{section_key}:** {section_info.get('instructions', '')}"
189
+ )
190
+ if section_info.get("depends_on"):
191
+ depends_on = section_info["depends_on"]
192
+ if isinstance(depends_on, list):
193
+ content_parts.append(
194
+ f" *(depends on: {', '.join(depends_on)})*"
195
+ )
196
+ content_parts.append("")
197
+
198
+ for section in artifact.get_ordered_sections():
199
+ content_parts.append(f"\n## {section.title}\n")
200
+ if section.content:
201
+ content_parts.append(f"{section.content}\n")
202
+
203
+ combined_content = "\n".join(content_parts)
204
+ logger.debug(
205
+ "📄 Read artifact with %d sections (%d characters)",
206
+ len(artifact.sections),
207
+ len(combined_content),
208
+ )
209
+ return combined_content
210
+
211
+ except Exception as e:
212
+ error_msg = f"Failed to read artifact '{artifact_id}': {str(e)}"
213
+ logger.error("❌ Read artifact failed: %s", error_msg)
214
+ return f"Error: {error_msg}"
215
+
216
+
217
+ async def write_artifact_section(
218
+ ctx: RunContext[AgentDeps],
219
+ artifact_id: str,
220
+ agent_mode: str,
221
+ section_number: int,
222
+ section_slug: str,
223
+ section_title: str,
224
+ content: str,
225
+ ) -> str:
226
+ """Write content to a specific section of an artifact.
227
+
228
+ Creates the artifact and/or section if they don't exist.
229
+
230
+ Args:
231
+ ctx: RunContext containing AgentDeps with artifact service
232
+ artifact_id: Artifact identifier
233
+ agent_mode: Agent mode (research, plan, tasks)
234
+ section_number: Section number (1, 2, 3, etc.)
235
+ section_slug: URL-friendly section identifier
236
+ section_title: Human-readable section title
237
+ content: Section content in markdown
238
+
239
+ Returns:
240
+ Success message or error message
241
+
242
+ Example:
243
+ write_artifact_section("market-analysis", "research", 1, "overview", "Market Overview", "...")
244
+ """
245
+ logger.debug(
246
+ "🔧 Writing to artifact section: %s/%s section %d",
247
+ agent_mode,
248
+ artifact_id,
249
+ section_number,
250
+ )
251
+
252
+ # Parse and validate agent mode
253
+ mode, error_msg = handle_agent_mode_parsing(agent_mode)
254
+ if error_msg:
255
+ logger.error("❌ Write artifact section failed: %s", error_msg)
256
+ return f"Error: {error_msg}"
257
+
258
+ # At this point, mode is guaranteed to be not None due to successful validation
259
+ if mode is None:
260
+ return "Error: Agent mode validation failed"
261
+
262
+ try:
263
+ service = ctx.deps.artifact_service
264
+
265
+ # Get or create the section
266
+ section, created = service.get_or_create_section(
267
+ artifact_id, mode, section_number, section_slug, section_title, content
268
+ )
269
+
270
+ # Track the section file operation
271
+ from shotgun.agents.models import FileOperationType
272
+ from shotgun.utils.file_system_utils import get_shotgun_base_path
273
+
274
+ section_path = (
275
+ get_shotgun_base_path()
276
+ / mode.value
277
+ / artifact_id
278
+ / f"{section_number:02d}_{section_slug}.md"
279
+ )
280
+ operation = FileOperationType.CREATED if created else FileOperationType.UPDATED
281
+ ctx.deps.file_tracker.add_operation(section_path, operation)
282
+
283
+ if created:
284
+ success_msg = (
285
+ f"Created section {section_number} '{section_title}' "
286
+ f"in artifact '{artifact_id}' with {len(content)} characters"
287
+ )
288
+ else:
289
+ # Update existing section content
290
+ service.update_section(artifact_id, mode, section_number, content=content)
291
+ success_msg = (
292
+ f"Updated section {section_number} '{section_title}' "
293
+ f"in artifact '{artifact_id}' with {len(content)} characters"
294
+ )
295
+
296
+ logger.debug("✅ %s", success_msg)
297
+ return success_msg
298
+
299
+ except Exception as e:
300
+ error_msg = (
301
+ f"Failed to write section {section_number} "
302
+ f"to artifact '{artifact_id}': {str(e)}"
303
+ )
304
+ logger.error("❌ Write artifact section failed: %s", error_msg)
305
+ return f"Error: {error_msg}"
306
+
307
+
308
+ async def read_artifact_section(
309
+ ctx: RunContext[AgentDeps],
310
+ artifact_id: str,
311
+ agent_mode: str,
312
+ section_number: int,
313
+ ) -> str:
314
+ """Read content from a specific section of an artifact.
315
+
316
+ Args:
317
+ ctx: RunContext containing AgentDeps with artifact service
318
+ artifact_id: Artifact identifier
319
+ agent_mode: Agent mode (research, plan, tasks)
320
+ section_number: Section number
321
+
322
+ Returns:
323
+ Section content or error message
324
+
325
+ Example:
326
+ read_artifact_section("market-analysis", "research", 1)
327
+ """
328
+ logger.debug(
329
+ "🔧 Reading artifact section: %s/%s section %d",
330
+ agent_mode,
331
+ artifact_id,
332
+ section_number,
333
+ )
334
+
335
+ # Parse and validate agent mode
336
+ mode, error_msg = handle_agent_mode_parsing(agent_mode)
337
+ if error_msg:
338
+ logger.error("❌ Read artifact section failed: %s", error_msg)
339
+ return f"Error: {error_msg}"
340
+
341
+ # At this point, mode is guaranteed to be not None due to successful validation
342
+ if mode is None:
343
+ return "Error: Agent mode validation failed"
344
+
345
+ try:
346
+ service = ctx.deps.artifact_service
347
+
348
+ section = service.get_section(artifact_id, mode, section_number)
349
+
350
+ # Return formatted content with title
351
+ formatted_content = f"# {section.title}\n\n{section.content}"
352
+ logger.debug(
353
+ "📄 Read section %d with %d characters",
354
+ section_number,
355
+ len(section.content),
356
+ )
357
+ return formatted_content
358
+
359
+ except Exception as e:
360
+ error_msg = (
361
+ f"Failed to read section {section_number} "
362
+ f"from artifact '{artifact_id}': {str(e)}"
363
+ )
364
+ logger.error("❌ Read artifact section failed: %s", error_msg)
365
+ return f"Error: {error_msg}"
366
+
367
+
368
+ async def list_artifacts(
369
+ ctx: RunContext[AgentDeps],
370
+ agent_mode: str | None = None,
371
+ ) -> str:
372
+ """List all artifacts, optionally filtered by agent mode.
373
+
374
+ Args:
375
+ ctx: RunContext containing AgentDeps with artifact service
376
+ agent_mode: Optional agent mode filter (research, plan, tasks)
377
+
378
+ Returns:
379
+ Formatted list of artifacts or error message
380
+
381
+ Example:
382
+ list_artifacts("research")
383
+ list_artifacts() # List all artifacts
384
+ """
385
+ logger.debug("🔧 Listing artifacts for mode: %s", agent_mode or "all")
386
+
387
+ try:
388
+ service = ctx.deps.artifact_service
389
+
390
+ mode = None
391
+ if agent_mode:
392
+ mode, error_msg = handle_agent_mode_parsing(agent_mode)
393
+ if error_msg:
394
+ logger.error("❌ List artifacts failed: %s", error_msg)
395
+ return f"Error: {error_msg}"
396
+
397
+ summaries = service.list_artifacts(mode)
398
+
399
+ if not summaries:
400
+ mode_text = f" for {agent_mode}" if agent_mode else ""
401
+ return f"No artifacts found{mode_text}."
402
+
403
+ # Format as table
404
+ lines = [
405
+ f"{'Agent':<10} {'ID':<25} {'Sections':<8} {'Updated'}",
406
+ "-" * 55,
407
+ ]
408
+
409
+ for summary in summaries:
410
+ lines.append(
411
+ f"{summary.agent_mode.value:<10} "
412
+ f"{summary.artifact_id[:25]:<25} "
413
+ f"{summary.section_count:<8} "
414
+ f"{summary.updated_at.strftime('%Y-%m-%d')}"
415
+ )
416
+
417
+ if len(summaries) > 0:
418
+ lines.append(f"\nTotal: {len(summaries)} artifacts")
419
+
420
+ result = "\n".join(lines)
421
+ logger.debug("📄 Listed %d artifacts", len(summaries))
422
+ return result
423
+
424
+ except Exception as e:
425
+ error_msg = f"Failed to list artifacts: {str(e)}"
426
+ logger.error("❌ List artifacts failed: %s", error_msg)
427
+ return f"Error: {error_msg}"
428
+
429
+
430
+ async def list_artifact_templates(
431
+ ctx: RunContext[AgentDeps],
432
+ agent_mode: str | None = None,
433
+ ) -> str:
434
+ """List available artifact templates, optionally filtered by agent mode.
435
+
436
+ Args:
437
+ ctx: RunContext containing AgentDeps with artifact service
438
+ agent_mode: Optional agent mode filter (research, plan, tasks)
439
+
440
+ Returns:
441
+ Formatted list of templates or error message
442
+
443
+ Example:
444
+ list_artifact_templates("research")
445
+ list_artifact_templates() # List all templates
446
+ """
447
+ logger.debug("🔧 Listing templates for mode: %s", agent_mode or "all")
448
+
449
+ try:
450
+ service = ctx.deps.artifact_service
451
+
452
+ mode = None
453
+ if agent_mode:
454
+ mode, error_msg = handle_agent_mode_parsing(agent_mode)
455
+ if error_msg:
456
+ logger.error("❌ List templates failed: %s", error_msg)
457
+ return f"Error: {error_msg}"
458
+
459
+ templates = service.list_templates(mode)
460
+
461
+ if not templates:
462
+ mode_text = f" for {agent_mode}" if agent_mode else ""
463
+ return f"No templates found{mode_text}."
464
+
465
+ # Format as list with template details
466
+ lines = ["Available Templates:"]
467
+
468
+ for template in templates:
469
+ lines.append(f"\n• {template.template_id}")
470
+ lines.append(f" Name: {template.name}")
471
+ lines.append(f" Mode: {template.agent_mode.value}")
472
+ lines.append(f" Purpose: {template.purpose}")
473
+ lines.append(f" Sections: {template.section_count}")
474
+
475
+ result = "\n".join(lines)
476
+ logger.debug("📄 Listed %d templates", len(templates))
477
+ return result
478
+
479
+ except Exception as e:
480
+ error_msg = f"Failed to list templates: {str(e)}"
481
+ logger.error("❌ List templates failed: %s", error_msg)
482
+ return f"Error: {error_msg}"
@@ -6,16 +6,15 @@ These tools are restricted to the .shotgun directory for security.
6
6
  from pathlib import Path
7
7
  from typing import Literal
8
8
 
9
+ from pydantic_ai import RunContext
10
+
11
+ from shotgun.agents.models import AgentDeps, FileOperationType
9
12
  from shotgun.logging_config import get_logger
13
+ from shotgun.utils.file_system_utils import get_shotgun_base_path
10
14
 
11
15
  logger = get_logger(__name__)
12
16
 
13
17
 
14
- def _get_shotgun_base_path() -> Path:
15
- """Get the absolute path to the .shotgun directory."""
16
- return Path.cwd() / ".shotgun"
17
-
18
-
19
18
  def _validate_shotgun_path(filename: str) -> Path:
20
19
  """Validate and resolve a file path within the .shotgun directory.
21
20
 
@@ -28,7 +27,7 @@ def _validate_shotgun_path(filename: str) -> Path:
28
27
  Raises:
29
28
  ValueError: If the path attempts to access files outside .shotgun directory
30
29
  """
31
- base_path = _get_shotgun_base_path()
30
+ base_path = get_shotgun_base_path()
32
31
 
33
32
  # Create the full path
34
33
  full_path = (base_path / filename).resolve()
@@ -44,7 +43,7 @@ def _validate_shotgun_path(filename: str) -> Path:
44
43
  return full_path
45
44
 
46
45
 
47
- def read_file(filename: str) -> str:
46
+ async def read_file(ctx: RunContext[AgentDeps], filename: str) -> str:
48
47
  """Read a file from the .shotgun directory.
49
48
 
50
49
  Args:
@@ -75,7 +74,12 @@ def read_file(filename: str) -> str:
75
74
  return error_msg
76
75
 
77
76
 
78
- def write_file(filename: str, content: str, mode: Literal["w", "a"] = "w") -> str:
77
+ async def write_file(
78
+ ctx: RunContext[AgentDeps],
79
+ filename: str,
80
+ content: str,
81
+ mode: Literal["w", "a"] = "w",
82
+ ) -> str:
79
83
  """Write content to a file in the .shotgun directory.
80
84
 
81
85
  Args:
@@ -97,6 +101,16 @@ def write_file(filename: str, content: str, mode: Literal["w", "a"] = "w") -> st
97
101
  try:
98
102
  file_path = _validate_shotgun_path(filename)
99
103
 
104
+ # Determine operation type
105
+ if mode == "a":
106
+ operation = FileOperationType.UPDATED
107
+ else:
108
+ operation = (
109
+ FileOperationType.CREATED
110
+ if not file_path.exists()
111
+ else FileOperationType.UPDATED
112
+ )
113
+
100
114
  # Ensure parent directory exists
101
115
  file_path.parent.mkdir(parents=True, exist_ok=True)
102
116
 
@@ -105,11 +119,16 @@ def write_file(filename: str, content: str, mode: Literal["w", "a"] = "w") -> st
105
119
  with open(file_path, "a", encoding="utf-8") as f:
106
120
  f.write(content)
107
121
  logger.debug("📄 Appended %d characters to %s", len(content), filename)
108
- return f"Successfully appended {len(content)} characters to {filename}"
122
+ result = f"Successfully appended {len(content)} characters to {filename}"
109
123
  else:
110
124
  file_path.write_text(content, encoding="utf-8")
111
125
  logger.debug("📄 Wrote %d characters to %s", len(content), filename)
112
- return f"Successfully wrote {len(content)} characters to {filename}"
126
+ result = f"Successfully wrote {len(content)} characters to {filename}"
127
+
128
+ # Track the file operation
129
+ ctx.deps.file_tracker.add_operation(file_path, operation)
130
+
131
+ return result
113
132
 
114
133
  except Exception as e:
115
134
  error_msg = f"Error writing file '{filename}': {str(e)}"
@@ -117,7 +136,7 @@ def write_file(filename: str, content: str, mode: Literal["w", "a"] = "w") -> st
117
136
  return error_msg
118
137
 
119
138
 
120
- def append_file(filename: str, content: str) -> str:
139
+ async def append_file(ctx: RunContext[AgentDeps], filename: str, content: str) -> str:
121
140
  """Append content to a file in the .shotgun directory.
122
141
 
123
142
  Args:
@@ -127,4 +146,4 @@ def append_file(filename: str, content: str) -> str:
127
146
  Returns:
128
147
  Success message or error message
129
148
  """
130
- return write_file(filename, content, mode="a")
149
+ return await write_file(ctx, filename, content, mode="a")