massgen 0.1.0a3__py3-none-any.whl → 0.1.1__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 massgen might be problematic. Click here for more details.

Files changed (111) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +17 -0
  3. massgen/api_params_handler/_api_params_handler_base.py +1 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +8 -1
  5. massgen/api_params_handler/_claude_api_params_handler.py +8 -1
  6. massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
  7. massgen/api_params_handler/_response_api_params_handler.py +8 -1
  8. massgen/backend/base.py +31 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +282 -11
  10. massgen/backend/chat_completions.py +182 -92
  11. massgen/backend/claude.py +115 -18
  12. massgen/backend/claude_code.py +378 -14
  13. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  14. massgen/backend/gemini.py +1275 -1607
  15. massgen/backend/gemini_mcp_manager.py +545 -0
  16. massgen/backend/gemini_trackers.py +344 -0
  17. massgen/backend/gemini_utils.py +43 -0
  18. massgen/backend/response.py +129 -70
  19. massgen/cli.py +577 -110
  20. massgen/config_builder.py +376 -27
  21. massgen/configs/README.md +111 -80
  22. massgen/configs/basic/multi/three_agents_default.yaml +1 -1
  23. massgen/configs/basic/single/single_agent.yaml +1 -1
  24. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  25. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  26. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  29. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  30. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  31. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  34. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  35. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  36. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  39. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  40. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  41. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  42. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  46. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  47. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  52. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  56. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  57. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  60. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  61. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  62. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  65. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  66. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  67. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  68. massgen/formatter/_chat_completions_formatter.py +104 -0
  69. massgen/formatter/_claude_formatter.py +120 -0
  70. massgen/formatter/_gemini_formatter.py +448 -0
  71. massgen/formatter/_response_formatter.py +88 -0
  72. massgen/frontend/coordination_ui.py +4 -2
  73. massgen/logger_config.py +35 -3
  74. massgen/message_templates.py +56 -6
  75. massgen/orchestrator.py +179 -10
  76. massgen/stream_chunk/base.py +3 -0
  77. massgen/tests/custom_tools_example.py +392 -0
  78. massgen/tests/mcp_test_server.py +17 -7
  79. massgen/tests/test_config_builder.py +423 -0
  80. massgen/tests/test_custom_tools.py +401 -0
  81. massgen/tests/test_tools.py +127 -0
  82. massgen/tool/README.md +935 -0
  83. massgen/tool/__init__.py +39 -0
  84. massgen/tool/_async_helpers.py +70 -0
  85. massgen/tool/_basic/__init__.py +8 -0
  86. massgen/tool/_basic/_two_num_tool.py +24 -0
  87. massgen/tool/_code_executors/__init__.py +10 -0
  88. massgen/tool/_code_executors/_python_executor.py +74 -0
  89. massgen/tool/_code_executors/_shell_executor.py +61 -0
  90. massgen/tool/_exceptions.py +39 -0
  91. massgen/tool/_file_handlers/__init__.py +10 -0
  92. massgen/tool/_file_handlers/_file_operations.py +218 -0
  93. massgen/tool/_manager.py +634 -0
  94. massgen/tool/_registered_tool.py +88 -0
  95. massgen/tool/_result.py +66 -0
  96. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  97. massgen/tool/docs/builtin_tools.md +681 -0
  98. massgen/tool/docs/exceptions.md +794 -0
  99. massgen/tool/docs/execution_results.md +691 -0
  100. massgen/tool/docs/manager.md +887 -0
  101. massgen/tool/docs/workflow_toolkits.md +529 -0
  102. massgen/tool/workflow_toolkits/__init__.py +57 -0
  103. massgen/tool/workflow_toolkits/base.py +55 -0
  104. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  105. massgen/tool/workflow_toolkits/vote.py +167 -0
  106. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
  107. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
  108. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
  109. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
  110. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
  111. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Tool module for MassGen framework."""
3
+
4
+ from ._code_executors import run_python_script, run_shell_script
5
+ from ._file_handlers import append_file_content, read_file_content, save_file_content
6
+ from ._manager import ToolManager
7
+ from ._result import ExecutionResult
8
+ from .workflow_toolkits import (
9
+ BaseToolkit,
10
+ NewAnswerToolkit,
11
+ ToolType,
12
+ VoteToolkit,
13
+ get_workflow_tools,
14
+ )
15
+
16
+ __all__ = [
17
+ "ToolManager",
18
+ "ExecutionResult",
19
+ "two_num_tool",
20
+ "run_python_script",
21
+ "run_shell_script",
22
+ "read_file_content",
23
+ "save_file_content",
24
+ "append_file_content",
25
+ "dashscope_generate_image",
26
+ "dashscope_generate_audio",
27
+ "dashscope_analyze_image",
28
+ "openai_generate_image",
29
+ "openai_generate_audio",
30
+ "openai_modify_image",
31
+ "openai_create_variation",
32
+ "openai_analyze_image",
33
+ "openai_transcribe_audio",
34
+ "BaseToolkit",
35
+ "ToolType",
36
+ "NewAnswerToolkit",
37
+ "VoteToolkit",
38
+ "get_workflow_tools",
39
+ ]
@@ -0,0 +1,70 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Async utility functions for wrapping different return types."""
3
+
4
+ import asyncio
5
+ from typing import AsyncGenerator, Callable, Generator, Optional
6
+
7
+ from ._result import ExecutionResult, TextContent
8
+
9
+
10
+ async def _apply_post_processing(
11
+ result: ExecutionResult,
12
+ processor: Optional[Callable[[ExecutionResult], Optional[ExecutionResult]]],
13
+ ) -> ExecutionResult:
14
+ """Apply post-processing to an execution result."""
15
+ if processor:
16
+ processed = processor(result)
17
+ if processed:
18
+ return processed
19
+ return result
20
+
21
+
22
+ async def wrap_object_async(
23
+ obj: ExecutionResult,
24
+ processor: Optional[Callable[[ExecutionResult], Optional[ExecutionResult]]],
25
+ ) -> AsyncGenerator[ExecutionResult, None]:
26
+ """Convert a single ExecutionResult to async generator."""
27
+ yield await _apply_post_processing(obj, processor)
28
+
29
+
30
+ async def wrap_sync_gen_async(
31
+ sync_gen: Generator[ExecutionResult, None, None],
32
+ processor: Optional[Callable[[ExecutionResult], Optional[ExecutionResult]]],
33
+ ) -> AsyncGenerator[ExecutionResult, None]:
34
+ """Convert sync generator to async generator."""
35
+ for chunk in sync_gen:
36
+ yield await _apply_post_processing(chunk, processor)
37
+
38
+
39
+ async def wrap_as_async_generator(
40
+ async_gen: AsyncGenerator[ExecutionResult, None],
41
+ processor: Optional[Callable[[ExecutionResult], Optional[ExecutionResult]]],
42
+ ) -> AsyncGenerator[ExecutionResult, None]:
43
+ """Wrap async generator with interruption handling."""
44
+
45
+ previous_chunk = None
46
+ try:
47
+ async for chunk in async_gen:
48
+ processed = await _apply_post_processing(chunk, processor)
49
+ yield processed
50
+ previous_chunk = processed
51
+
52
+ except asyncio.CancelledError:
53
+ interrupt_msg = TextContent(
54
+ data="<system>Execution interrupted by user request</system>",
55
+ )
56
+
57
+ if previous_chunk:
58
+ previous_chunk.output_blocks.append(interrupt_msg)
59
+ previous_chunk.was_interrupted = True
60
+ previous_chunk.is_final = True
61
+ yield await _apply_post_processing(previous_chunk, processor)
62
+ else:
63
+ yield await _apply_post_processing(
64
+ ExecutionResult(
65
+ output_blocks=[interrupt_msg],
66
+ was_interrupted=True,
67
+ is_final=True,
68
+ ),
69
+ processor,
70
+ )
@@ -0,0 +1,8 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Basic tools."""
3
+
4
+ from ._two_num_tool import two_num_tool
5
+
6
+ __all__ = [
7
+ "two_num_tool",
8
+ ]
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Sample math tool for testing purposes.
4
+ """
5
+
6
+ from massgen.tool._result import ExecutionResult, TextContent
7
+
8
+
9
+ async def two_num_tool(x: int, y: int) -> ExecutionResult:
10
+ """Add two numbers together.
11
+
12
+ Args:
13
+ x: First number
14
+ y: Second number
15
+
16
+ Returns:
17
+ Sum of the two numbers
18
+ """
19
+ result = x + y
20
+ return ExecutionResult(
21
+ output_blocks=[
22
+ TextContent(data=f"The sum of {x} and {y} is {result}"),
23
+ ],
24
+ )
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Code execution tools."""
3
+
4
+ from ._python_executor import run_python_script
5
+ from ._shell_executor import run_shell_script
6
+
7
+ __all__ = [
8
+ "run_python_script",
9
+ "run_shell_script",
10
+ ]
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Python code execution tool implementation."""
3
+
4
+ import asyncio
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ import uuid
9
+ from typing import Any
10
+
11
+ from .._result import ExecutionResult, TextContent
12
+
13
+
14
+ async def run_python_script(
15
+ source_code: str,
16
+ max_duration: float = 300,
17
+ **extra_kwargs: Any,
18
+ ) -> ExecutionResult:
19
+ """Execute Python code in an isolated temporary environment.
20
+
21
+ The code runs in a temporary file that is cleaned up after execution.
22
+ Results must be printed to be captured in the output.
23
+
24
+ Args:
25
+ source_code: Python code to execute
26
+ max_duration: Maximum execution time in seconds (default: 300)
27
+
28
+ Returns:
29
+ ExecutionResult containing execution status and output
30
+ """
31
+
32
+ with tempfile.TemporaryDirectory() as temp_workspace:
33
+ script_file = os.path.join(temp_workspace, f"script_{uuid.uuid4().hex}.py")
34
+ with open(script_file, "w", encoding="utf-8") as file:
35
+ file.write(source_code)
36
+
37
+ process = await asyncio.create_subprocess_exec(
38
+ sys.executable,
39
+ "-u",
40
+ script_file,
41
+ stdout=asyncio.subprocess.PIPE,
42
+ stderr=asyncio.subprocess.PIPE,
43
+ )
44
+
45
+ try:
46
+ await asyncio.wait_for(process.wait(), timeout=max_duration)
47
+ stdout_data, stderr_data = await process.communicate()
48
+ stdout_text = stdout_data.decode("utf-8")
49
+ stderr_text = stderr_data.decode("utf-8")
50
+ exit_code = process.returncode
51
+
52
+ except asyncio.TimeoutError:
53
+ timeout_msg = f"TimeoutError: Execution exceeded {max_duration} seconds limit"
54
+ exit_code = -1
55
+ try:
56
+ process.terminate()
57
+ stdout_data, stderr_data = await process.communicate()
58
+ stdout_text = stdout_data.decode("utf-8")
59
+ stderr_text = stderr_data.decode("utf-8")
60
+ if stderr_text:
61
+ stderr_text += f"\n{timeout_msg}"
62
+ else:
63
+ stderr_text = timeout_msg
64
+ except ProcessLookupError:
65
+ stdout_text = ""
66
+ stderr_text = timeout_msg
67
+
68
+ return ExecutionResult(
69
+ output_blocks=[
70
+ TextContent(
71
+ data=(f"<exit_code>{exit_code}</exit_code>" f"<stdout>{stdout_text}</stdout>" f"<stderr>{stderr_text}</stderr>"),
72
+ ),
73
+ ],
74
+ )
@@ -0,0 +1,61 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Shell command execution tool implementation."""
3
+
4
+ import asyncio
5
+ from typing import Any
6
+
7
+ from .._result import ExecutionResult, TextContent
8
+
9
+
10
+ async def run_shell_script(
11
+ shell_command: str,
12
+ max_duration: int = 300,
13
+ **extra_kwargs: Any,
14
+ ) -> ExecutionResult:
15
+ """Execute shell commands and capture output.
16
+
17
+ Args:
18
+ shell_command: Shell command to execute
19
+ max_duration: Maximum execution time in seconds (default: 300)
20
+
21
+ Returns:
22
+ ExecutionResult with exit code, stdout, and stderr
23
+ """
24
+
25
+ process = await asyncio.create_subprocess_shell(
26
+ shell_command,
27
+ stdout=asyncio.subprocess.PIPE,
28
+ stderr=asyncio.subprocess.PIPE,
29
+ bufsize=0,
30
+ )
31
+
32
+ try:
33
+ await asyncio.wait_for(process.wait(), timeout=max_duration)
34
+ stdout_data, stderr_data = await process.communicate()
35
+ stdout_text = stdout_data.decode("utf-8")
36
+ stderr_text = stderr_data.decode("utf-8")
37
+ exit_code = process.returncode
38
+
39
+ except asyncio.TimeoutError:
40
+ timeout_msg = f"TimeoutError: Command execution exceeded {max_duration} seconds limit"
41
+ exit_code = -1
42
+ try:
43
+ process.terminate()
44
+ stdout_data, stderr_data = await process.communicate()
45
+ stdout_text = stdout_data.decode("utf-8")
46
+ stderr_text = stderr_data.decode("utf-8")
47
+ if stderr_text:
48
+ stderr_text += f"\n{timeout_msg}"
49
+ else:
50
+ stderr_text = timeout_msg
51
+ except ProcessLookupError:
52
+ stdout_text = ""
53
+ stderr_text = timeout_msg
54
+
55
+ return ExecutionResult(
56
+ output_blocks=[
57
+ TextContent(
58
+ data=(f"<exit_code>{exit_code}</exit_code>" f"<stdout>{stdout_text}</stdout>" f"<stderr>{stderr_text}</stderr>"),
59
+ ),
60
+ ],
61
+ )
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Exception classes for tool operations."""
3
+
4
+
5
+ class ToolException(Exception):
6
+ """Base exception for tool-related errors."""
7
+
8
+
9
+ class InvalidToolArgumentsException(ToolException):
10
+ """Raised when tool receives invalid arguments."""
11
+
12
+ def __init__(self, error_msg: str):
13
+ self.error_msg = error_msg
14
+ super().__init__(self.error_msg)
15
+
16
+
17
+ class ToolNotFoundException(ToolException):
18
+ """Raised when requested tool is not found."""
19
+
20
+ def __init__(self, tool_name: str):
21
+ self.tool_name = tool_name
22
+ super().__init__(f"Tool '{tool_name}' not found in registry")
23
+
24
+
25
+ class ToolExecutionException(ToolException):
26
+ """Raised when tool execution fails."""
27
+
28
+ def __init__(self, tool_name: str, error_details: str):
29
+ self.tool_name = tool_name
30
+ self.error_details = error_details
31
+ super().__init__(f"Tool '{tool_name}' execution failed: {error_details}")
32
+
33
+
34
+ class CategoryNotFoundException(ToolException):
35
+ """Raised when tool category is not found."""
36
+
37
+ def __init__(self, category_name: str):
38
+ self.category_name = category_name
39
+ super().__init__(f"Category '{category_name}' not found")
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ """File handling tools."""
3
+
4
+ from ._file_operations import append_file_content, read_file_content, save_file_content
5
+
6
+ __all__ = [
7
+ "read_file_content",
8
+ "save_file_content",
9
+ "append_file_content",
10
+ ]
@@ -0,0 +1,218 @@
1
+ # -*- coding: utf-8 -*-
2
+ """File operation tools for reading and writing."""
3
+
4
+ import os
5
+ from typing import List, Optional
6
+
7
+ from .._result import ExecutionResult, TextContent
8
+
9
+
10
+ async def read_file_content(
11
+ target_path: str,
12
+ line_range: Optional[List[int]] = None,
13
+ ) -> ExecutionResult:
14
+ """Read file contents with optional line range specification.
15
+
16
+ Args:
17
+ target_path: Path to the file to read
18
+ line_range: Optional [start, end] line numbers (1-based, inclusive).
19
+ Use negative numbers for lines from end (e.g., [-100, -1])
20
+
21
+ Returns:
22
+ ExecutionResult with file content or error message
23
+ """
24
+ if not os.path.exists(target_path):
25
+ return ExecutionResult(
26
+ output_blocks=[
27
+ TextContent(
28
+ data=f"Error: File '{target_path}' does not exist.",
29
+ ),
30
+ ],
31
+ )
32
+
33
+ if not os.path.isfile(target_path):
34
+ return ExecutionResult(
35
+ output_blocks=[
36
+ TextContent(
37
+ data=f"Error: Path '{target_path}' is not a file.",
38
+ ),
39
+ ],
40
+ )
41
+
42
+ try:
43
+ with open(target_path, "r", encoding="utf-8") as file:
44
+ all_lines = file.readlines()
45
+
46
+ if line_range:
47
+ start_line, end_line = line_range
48
+
49
+ # Handle negative indices
50
+ if start_line < 0:
51
+ start_line = len(all_lines) + start_line + 1
52
+ if end_line < 0:
53
+ end_line = len(all_lines) + end_line + 1
54
+
55
+ # Validate range
56
+ if start_line < 1 or end_line > len(all_lines) or start_line > end_line:
57
+ return ExecutionResult(
58
+ output_blocks=[
59
+ TextContent(
60
+ data=f"Error: Invalid line range {line_range} for file with {len(all_lines)} lines.",
61
+ ),
62
+ ],
63
+ )
64
+
65
+ # Extract lines (convert to 0-based indexing)
66
+ selected_lines = all_lines[start_line - 1 : end_line]
67
+ content_with_numbers = "".join(f"{i + start_line:4d}│ {line}" for i, line in enumerate(selected_lines))
68
+
69
+ return ExecutionResult(
70
+ output_blocks=[
71
+ TextContent(
72
+ data=f"Content of {target_path} (lines {start_line}-{end_line}):\n```\n{content_with_numbers}```",
73
+ ),
74
+ ],
75
+ )
76
+ else:
77
+ full_content = "".join(all_lines)
78
+ return ExecutionResult(
79
+ output_blocks=[
80
+ TextContent(
81
+ data=f"Content of {target_path}:\n```\n{full_content}```",
82
+ ),
83
+ ],
84
+ )
85
+
86
+ except Exception as error:
87
+ return ExecutionResult(
88
+ output_blocks=[
89
+ TextContent(
90
+ data=f"Error reading file: {error}",
91
+ ),
92
+ ],
93
+ )
94
+
95
+
96
+ async def save_file_content(
97
+ target_path: str,
98
+ file_content: str,
99
+ create_dirs: bool = True,
100
+ ) -> ExecutionResult:
101
+ """Write content to a file, creating directories if needed.
102
+
103
+ Args:
104
+ target_path: Path where file will be saved
105
+ file_content: Content to write to the file
106
+ create_dirs: Whether to create parent directories if they don't exist
107
+
108
+ Returns:
109
+ ExecutionResult indicating success or failure
110
+ """
111
+ try:
112
+ # Create parent directories if requested
113
+ if create_dirs:
114
+ parent_dir = os.path.dirname(target_path)
115
+ if parent_dir:
116
+ os.makedirs(parent_dir, exist_ok=True)
117
+
118
+ with open(target_path, "w", encoding="utf-8") as file:
119
+ file.write(file_content)
120
+
121
+ return ExecutionResult(
122
+ output_blocks=[
123
+ TextContent(
124
+ data=f"Successfully wrote {len(file_content)} characters to {target_path}",
125
+ ),
126
+ ],
127
+ )
128
+
129
+ except Exception as error:
130
+ return ExecutionResult(
131
+ output_blocks=[
132
+ TextContent(
133
+ data=f"Error writing file: {error}",
134
+ ),
135
+ ],
136
+ )
137
+
138
+
139
+ async def append_file_content(
140
+ target_path: str,
141
+ additional_content: str,
142
+ line_position: Optional[int] = None,
143
+ ) -> ExecutionResult:
144
+ """Append content to a file or insert at specific line.
145
+
146
+ Args:
147
+ target_path: Path to the file
148
+ additional_content: Content to append or insert
149
+ line_position: Optional line number to insert at (1-based).
150
+ If None, appends to end of file.
151
+
152
+ Returns:
153
+ ExecutionResult indicating success or failure
154
+ """
155
+ if not os.path.exists(target_path):
156
+ return ExecutionResult(
157
+ output_blocks=[
158
+ TextContent(
159
+ data=f"Error: File '{target_path}' does not exist.",
160
+ ),
161
+ ],
162
+ )
163
+
164
+ try:
165
+ with open(target_path, "r", encoding="utf-8") as file:
166
+ current_lines = file.readlines()
167
+
168
+ if line_position is None:
169
+ # Append to end
170
+ with open(target_path, "a", encoding="utf-8") as file:
171
+ file.write(additional_content)
172
+
173
+ return ExecutionResult(
174
+ output_blocks=[
175
+ TextContent(
176
+ data=f"Successfully appended {len(additional_content)} characters to {target_path}",
177
+ ),
178
+ ],
179
+ )
180
+ else:
181
+ # Insert at specific line
182
+ if line_position < 1 or line_position > len(current_lines) + 1:
183
+ return ExecutionResult(
184
+ output_blocks=[
185
+ TextContent(
186
+ data=f"Error: Invalid line position {line_position} for file with {len(current_lines)} lines.",
187
+ ),
188
+ ],
189
+ )
190
+
191
+ # Insert content (convert to 0-based index)
192
+ insert_idx = line_position - 1
193
+
194
+ # Ensure content ends with newline for proper insertion
195
+ if not additional_content.endswith("\n"):
196
+ additional_content += "\n"
197
+
198
+ current_lines.insert(insert_idx, additional_content)
199
+
200
+ with open(target_path, "w", encoding="utf-8") as file:
201
+ file.writelines(current_lines)
202
+
203
+ return ExecutionResult(
204
+ output_blocks=[
205
+ TextContent(
206
+ data=f"Successfully inserted content at line {line_position} in {target_path}",
207
+ ),
208
+ ],
209
+ )
210
+
211
+ except Exception as error:
212
+ return ExecutionResult(
213
+ output_blocks=[
214
+ TextContent(
215
+ data=f"Error modifying file: {error}",
216
+ ),
217
+ ],
218
+ )