EvoScientist 0.0.1.dev2__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.
Files changed (107) hide show
  1. EvoScientist/EvoScientist.py +157 -0
  2. EvoScientist/__init__.py +24 -0
  3. EvoScientist/__main__.py +4 -0
  4. EvoScientist/backends.py +392 -0
  5. EvoScientist/cli.py +1553 -0
  6. EvoScientist/middleware.py +35 -0
  7. EvoScientist/prompts.py +277 -0
  8. EvoScientist/skills/accelerate/SKILL.md +332 -0
  9. EvoScientist/skills/accelerate/references/custom-plugins.md +453 -0
  10. EvoScientist/skills/accelerate/references/megatron-integration.md +489 -0
  11. EvoScientist/skills/accelerate/references/performance.md +525 -0
  12. EvoScientist/skills/bitsandbytes/SKILL.md +411 -0
  13. EvoScientist/skills/bitsandbytes/references/memory-optimization.md +521 -0
  14. EvoScientist/skills/bitsandbytes/references/qlora-training.md +521 -0
  15. EvoScientist/skills/bitsandbytes/references/quantization-formats.md +447 -0
  16. EvoScientist/skills/find-skills/SKILL.md +133 -0
  17. EvoScientist/skills/find-skills/scripts/install_skill.py +211 -0
  18. EvoScientist/skills/flash-attention/SKILL.md +367 -0
  19. EvoScientist/skills/flash-attention/references/benchmarks.md +215 -0
  20. EvoScientist/skills/flash-attention/references/transformers-integration.md +293 -0
  21. EvoScientist/skills/llama-cpp/SKILL.md +258 -0
  22. EvoScientist/skills/llama-cpp/references/optimization.md +89 -0
  23. EvoScientist/skills/llama-cpp/references/quantization.md +213 -0
  24. EvoScientist/skills/llama-cpp/references/server.md +125 -0
  25. EvoScientist/skills/lm-evaluation-harness/SKILL.md +490 -0
  26. EvoScientist/skills/lm-evaluation-harness/references/api-evaluation.md +490 -0
  27. EvoScientist/skills/lm-evaluation-harness/references/benchmark-guide.md +488 -0
  28. EvoScientist/skills/lm-evaluation-harness/references/custom-tasks.md +602 -0
  29. EvoScientist/skills/lm-evaluation-harness/references/distributed-eval.md +519 -0
  30. EvoScientist/skills/ml-paper-writing/SKILL.md +937 -0
  31. EvoScientist/skills/ml-paper-writing/references/checklists.md +361 -0
  32. EvoScientist/skills/ml-paper-writing/references/citation-workflow.md +562 -0
  33. EvoScientist/skills/ml-paper-writing/references/reviewer-guidelines.md +367 -0
  34. EvoScientist/skills/ml-paper-writing/references/sources.md +159 -0
  35. EvoScientist/skills/ml-paper-writing/references/writing-guide.md +476 -0
  36. EvoScientist/skills/ml-paper-writing/templates/README.md +251 -0
  37. EvoScientist/skills/ml-paper-writing/templates/aaai2026/README.md +534 -0
  38. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  39. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  40. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bib +111 -0
  41. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bst +1493 -0
  42. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.sty +315 -0
  43. EvoScientist/skills/ml-paper-writing/templates/acl/README.md +50 -0
  44. EvoScientist/skills/ml-paper-writing/templates/acl/acl.sty +312 -0
  45. EvoScientist/skills/ml-paper-writing/templates/acl/acl_latex.tex +377 -0
  46. EvoScientist/skills/ml-paper-writing/templates/acl/acl_lualatex.tex +101 -0
  47. EvoScientist/skills/ml-paper-writing/templates/acl/acl_natbib.bst +1940 -0
  48. EvoScientist/skills/ml-paper-writing/templates/acl/anthology.bib.txt +26 -0
  49. EvoScientist/skills/ml-paper-writing/templates/acl/custom.bib +70 -0
  50. EvoScientist/skills/ml-paper-writing/templates/acl/formatting.md +326 -0
  51. EvoScientist/skills/ml-paper-writing/templates/colm2025/README.md +3 -0
  52. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bib +11 -0
  53. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bst +1440 -0
  54. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
  55. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.sty +218 -0
  56. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.tex +305 -0
  57. EvoScientist/skills/ml-paper-writing/templates/colm2025/fancyhdr.sty +485 -0
  58. EvoScientist/skills/ml-paper-writing/templates/colm2025/math_commands.tex +508 -0
  59. EvoScientist/skills/ml-paper-writing/templates/colm2025/natbib.sty +1246 -0
  60. EvoScientist/skills/ml-paper-writing/templates/iclr2026/fancyhdr.sty +485 -0
  61. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bib +24 -0
  62. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bst +1440 -0
  63. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
  64. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.sty +246 -0
  65. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.tex +414 -0
  66. EvoScientist/skills/ml-paper-writing/templates/iclr2026/math_commands.tex +508 -0
  67. EvoScientist/skills/ml-paper-writing/templates/iclr2026/natbib.sty +1246 -0
  68. EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithm.sty +79 -0
  69. EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithmic.sty +201 -0
  70. EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.bib +75 -0
  71. EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.pdf +0 -0
  72. EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.tex +662 -0
  73. EvoScientist/skills/ml-paper-writing/templates/icml2026/fancyhdr.sty +864 -0
  74. EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.bst +1443 -0
  75. EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.sty +767 -0
  76. EvoScientist/skills/ml-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
  77. EvoScientist/skills/ml-paper-writing/templates/neurips2025/Makefile +36 -0
  78. EvoScientist/skills/ml-paper-writing/templates/neurips2025/extra_pkgs.tex +53 -0
  79. EvoScientist/skills/ml-paper-writing/templates/neurips2025/main.tex +38 -0
  80. EvoScientist/skills/ml-paper-writing/templates/neurips2025/neurips.sty +382 -0
  81. EvoScientist/skills/peft/SKILL.md +431 -0
  82. EvoScientist/skills/peft/references/advanced-usage.md +514 -0
  83. EvoScientist/skills/peft/references/troubleshooting.md +480 -0
  84. EvoScientist/skills/ray-data/SKILL.md +326 -0
  85. EvoScientist/skills/ray-data/references/integration.md +82 -0
  86. EvoScientist/skills/ray-data/references/transformations.md +83 -0
  87. EvoScientist/skills/skill-creator/LICENSE.txt +202 -0
  88. EvoScientist/skills/skill-creator/SKILL.md +356 -0
  89. EvoScientist/skills/skill-creator/references/output-patterns.md +82 -0
  90. EvoScientist/skills/skill-creator/references/workflows.md +28 -0
  91. EvoScientist/skills/skill-creator/scripts/init_skill.py +303 -0
  92. EvoScientist/skills/skill-creator/scripts/package_skill.py +110 -0
  93. EvoScientist/skills/skill-creator/scripts/quick_validate.py +95 -0
  94. EvoScientist/stream/__init__.py +53 -0
  95. EvoScientist/stream/emitter.py +94 -0
  96. EvoScientist/stream/formatter.py +168 -0
  97. EvoScientist/stream/tracker.py +115 -0
  98. EvoScientist/stream/utils.py +255 -0
  99. EvoScientist/subagent.yaml +147 -0
  100. EvoScientist/tools.py +135 -0
  101. EvoScientist/utils.py +207 -0
  102. evoscientist-0.0.1.dev2.dist-info/METADATA +227 -0
  103. evoscientist-0.0.1.dev2.dist-info/RECORD +107 -0
  104. evoscientist-0.0.1.dev2.dist-info/WHEEL +5 -0
  105. evoscientist-0.0.1.dev2.dist-info/entry_points.txt +5 -0
  106. evoscientist-0.0.1.dev2.dist-info/licenses/LICENSE +21 -0
  107. evoscientist-0.0.1.dev2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,157 @@
1
+ """EvoScientist Agent graph construction.
2
+
3
+ This module creates and exports the compiled agent graph.
4
+ Usage:
5
+ from EvoScientist import agent
6
+
7
+ # Notebook / programmatic usage
8
+ for state in agent.stream(
9
+ {"messages": [HumanMessage(content="your question")]},
10
+ config={"configurable": {"thread_id": "1"}},
11
+ stream_mode="values",
12
+ ):
13
+ ...
14
+ """
15
+
16
+ import os
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+
20
+ from deepagents import create_deep_agent
21
+ from deepagents.backends import FilesystemBackend, CompositeBackend
22
+ from langchain.chat_models import init_chat_model
23
+
24
+ from .backends import CustomSandboxBackend, MergedReadOnlyBackend
25
+ from .middleware import create_skills_middleware
26
+ from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt
27
+ from .utils import load_subagents
28
+ from .tools import tavily_search, think_tool
29
+
30
+ # =============================================================================
31
+ # Configuration
32
+ # =============================================================================
33
+
34
+ # Backend mode: "sandbox" (with execute) or "filesystem" (read/write only)
35
+ BACKEND_MODE = "sandbox"
36
+
37
+ # Research limits
38
+ MAX_CONCURRENT = 3 # Max parallel sub-agents
39
+ MAX_ITERATIONS = 3 # Max delegation rounds
40
+
41
+ # Workspace settings
42
+ WORKSPACE_DIR = "./workspace/"
43
+ SKILLS_DIR = str(Path(__file__).parent / "skills")
44
+ SUBAGENTS_CONFIG = Path(__file__).parent / "subagent.yaml"
45
+
46
+ # =============================================================================
47
+ # Initialization
48
+ # =============================================================================
49
+
50
+ # Get current date
51
+ current_date = datetime.now().strftime("%Y-%m-%d")
52
+
53
+ # Generate system prompt with limits
54
+ SYSTEM_PROMPT = get_system_prompt(
55
+ max_concurrent=MAX_CONCURRENT,
56
+ max_iterations=MAX_ITERATIONS,
57
+ )
58
+
59
+ # Initialize chat model
60
+ chat_model = init_chat_model(
61
+ model="claude-sonnet-4-5-20250929",
62
+ model_provider="anthropic",
63
+ # thinking={"type": "enabled", "budget_tokens": 2000},
64
+ )
65
+
66
+ # Initialize workspace backend based on mode
67
+ if BACKEND_MODE == "sandbox":
68
+ _workspace_backend = CustomSandboxBackend(
69
+ root_dir=WORKSPACE_DIR,
70
+ virtual_mode=True,
71
+ timeout=300,
72
+ )
73
+ else:
74
+ _workspace_backend = FilesystemBackend(
75
+ root_dir=WORKSPACE_DIR,
76
+ virtual_mode=True,
77
+ )
78
+
79
+ # Skills backend: merge user-installed (workspace) and system (package) skills
80
+ _skills_backend = MergedReadOnlyBackend(
81
+ primary_dir=str(Path(WORKSPACE_DIR) / "skills"), # user-installed, takes priority
82
+ secondary_dir=SKILLS_DIR, # package built-in, fallback
83
+ )
84
+
85
+ # Composite backend: workspace as default, skills mounted at /skills/
86
+ backend = CompositeBackend(
87
+ default=_workspace_backend,
88
+ routes={"/skills/": _skills_backend},
89
+ )
90
+
91
+ tool_registry = {
92
+ "think_tool": think_tool,
93
+ "tavily_search": tavily_search,
94
+ }
95
+
96
+ prompt_refs = {
97
+ "RESEARCHER_INSTRUCTIONS": RESEARCHER_INSTRUCTIONS.format(date=current_date),
98
+ }
99
+
100
+ subagents = load_subagents(
101
+ SUBAGENTS_CONFIG,
102
+ tool_registry=tool_registry,
103
+ prompt_refs=prompt_refs,
104
+ )
105
+
106
+ # Shared kwargs for agent creation
107
+ _AGENT_KWARGS = dict(
108
+ name="EvoScientist",
109
+ model=chat_model,
110
+ tools=[think_tool],
111
+ backend=backend,
112
+ subagents=subagents,
113
+ middleware=[create_skills_middleware(SKILLS_DIR, WORKSPACE_DIR)],
114
+ system_prompt=SYSTEM_PROMPT,
115
+ )
116
+
117
+ # Default agent (no checkpointer) — used by langgraph dev / LangSmith / notebooks
118
+ EvoScientist_agent = create_deep_agent(**_AGENT_KWARGS).with_config({"recursion_limit": 500})
119
+
120
+
121
+ def create_cli_agent(workspace_dir: str | None = None):
122
+ """Create agent with InMemorySaver checkpointer for CLI multi-turn support.
123
+
124
+ Args:
125
+ workspace_dir: Optional per-session workspace directory. If provided,
126
+ creates a fresh backend rooted at this path. If None, uses the
127
+ module-level default backend (./workspace/).
128
+ """
129
+ from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped]
130
+
131
+ if workspace_dir:
132
+ ws_backend = CustomSandboxBackend(
133
+ root_dir=workspace_dir,
134
+ virtual_mode=True,
135
+ timeout=300,
136
+ )
137
+ sk_backend = MergedReadOnlyBackend(
138
+ primary_dir=str(Path(workspace_dir) / "skills"),
139
+ secondary_dir=SKILLS_DIR,
140
+ )
141
+ be = CompositeBackend(
142
+ default=ws_backend,
143
+ routes={"/skills/": sk_backend},
144
+ )
145
+ mw = [create_skills_middleware(SKILLS_DIR, workspace_dir)]
146
+ kwargs = dict(
147
+ _AGENT_KWARGS,
148
+ backend=be,
149
+ middleware=mw,
150
+ )
151
+ else:
152
+ kwargs = dict(_AGENT_KWARGS)
153
+
154
+ return create_deep_agent(
155
+ **kwargs,
156
+ checkpointer=InMemorySaver(),
157
+ ).with_config({"recursion_limit": 500})
@@ -0,0 +1,24 @@
1
+ """EvoScientist Agent — AI-powered research & code execution."""
2
+
3
+ from .backends import CustomSandboxBackend, ReadOnlyFilesystemBackend
4
+ from .middleware import create_skills_middleware
5
+ from .prompts import get_system_prompt, RESEARCHER_INSTRUCTIONS
6
+ from .tools import tavily_search, think_tool
7
+ from .EvoScientist import EvoScientist_agent, create_cli_agent
8
+
9
+ __all__ = [
10
+ # Agent graph (main export)
11
+ "EvoScientist_agent",
12
+ "create_cli_agent",
13
+ # Backends
14
+ "CustomSandboxBackend",
15
+ "ReadOnlyFilesystemBackend",
16
+ # Middleware
17
+ "create_skills_middleware",
18
+ # Prompts
19
+ "get_system_prompt",
20
+ "RESEARCHER_INSTRUCTIONS",
21
+ # Tools
22
+ "tavily_search",
23
+ "think_tool",
24
+ ]
@@ -0,0 +1,4 @@
1
+ """Enable `python -m EvoScientist` execution."""
2
+ from EvoScientist.cli import main
3
+
4
+ main()
@@ -0,0 +1,392 @@
1
+ """Custom backends for EvoScientist agent."""
2
+
3
+ import os
4
+ import re
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from deepagents.backends import FilesystemBackend
9
+ from deepagents.backends.filesystem import WriteResult, EditResult
10
+ from deepagents.backends.protocol import (
11
+ ExecuteResponse,
12
+ FileDownloadResponse,
13
+ FileUploadResponse,
14
+ SandboxBackendProtocol,
15
+ )
16
+
17
+ # System path prefixes that should never appear in virtual paths.
18
+ # If the agent hallucinates an absolute system path, we block it.
19
+ _SYSTEM_PATH_PREFIXES = (
20
+ "/Users/", "/home/", "/tmp/", "/var/", "/etc/",
21
+ "/opt/", "/usr/", "/bin/", "/sbin/", "/dev/",
22
+ "/proc/", "/sys/", "/root/",
23
+ )
24
+
25
+ # Dangerous patterns that could escape the workspace
26
+ BLOCKED_PATTERNS = [
27
+ r'\.\.', # ../ directory traversal
28
+ r'~/', # home directory
29
+ r'\bcd\s+/', # cd to absolute path
30
+ r'\brm\s+-rf\s+/', # rm -rf with absolute path
31
+ ]
32
+
33
+ # Dangerous commands that should never be executed
34
+ BLOCKED_COMMANDS = [
35
+ 'sudo',
36
+ 'chmod',
37
+ 'chown',
38
+ 'mkfs',
39
+ 'dd',
40
+ 'shutdown',
41
+ 'reboot',
42
+ ]
43
+
44
+
45
+ def validate_command(command: str) -> str | None:
46
+ """
47
+ Validate a shell command for safety.
48
+
49
+ Returns:
50
+ None if command is safe, error message string if blocked.
51
+ """
52
+ # Check for directory traversal and dangerous patterns
53
+ for pattern in BLOCKED_PATTERNS:
54
+ if re.search(pattern, command):
55
+ return (
56
+ f"Command blocked: contains forbidden pattern '{pattern}'. "
57
+ f"All commands must operate within the workspace directory. "
58
+ f"Use relative paths (e.g., './file.py') instead."
59
+ )
60
+
61
+ # Check for dangerous commands
62
+ for cmd in BLOCKED_COMMANDS:
63
+ if re.search(rf'\b{cmd}\b', command):
64
+ return (
65
+ f"Command blocked: '{cmd}' is not allowed in sandbox mode. "
66
+ f"Only standard development commands are permitted."
67
+ )
68
+
69
+ return None
70
+
71
+
72
+ def convert_virtual_paths_in_command(command: str) -> str:
73
+ """
74
+ Convert virtual paths (starting with /) in commands to relative paths.
75
+
76
+ Examples:
77
+ - "python /main.py" -> "python ./main.py"
78
+ - "cat /data/file.txt" -> "cat ./data/file.txt"
79
+ - "ls /" -> "ls ."
80
+ - "python main.py" -> "python main.py" (unchanged)
81
+
82
+ Args:
83
+ command: Original command
84
+
85
+ Returns:
86
+ Converted command
87
+ """
88
+
89
+ def replace_virtual_path(match):
90
+ path = match.group(0)
91
+
92
+ # Skip content that looks like a URL
93
+ if '://' in command[max(0, match.start() - 10):match.end() + 10]:
94
+ return path
95
+
96
+ # Convert virtual path
97
+ if path == '/':
98
+ return '.'
99
+ else:
100
+ return '.' + path
101
+
102
+ # Match pattern: paths starting with / (but not URLs)
103
+ pattern = r'(?<=\s)/[^\s;|&<>\'"`]*|^/[^\s;|&<>\'"`]*'
104
+ converted = re.sub(pattern, replace_virtual_path, command)
105
+
106
+ return converted
107
+
108
+
109
+ class ReadOnlyFilesystemBackend(FilesystemBackend):
110
+ """
111
+ Read-only filesystem backend.
112
+
113
+ Allows read, ls, grep, glob operations but blocks write and edit.
114
+ Used for skills directory — agent can read skill definitions but cannot
115
+ modify them.
116
+ """
117
+
118
+ def write(self, file_path: str, content: str) -> WriteResult:
119
+ return WriteResult(
120
+ error="This directory is read-only. Write operations are not permitted here."
121
+ )
122
+
123
+ def edit(
124
+ self,
125
+ file_path: str,
126
+ old_string: str,
127
+ new_string: str,
128
+ replace_all: bool = False,
129
+ ) -> EditResult:
130
+ return EditResult(
131
+ error="This directory is read-only. Edit operations are not permitted here."
132
+ )
133
+
134
+
135
+ class MergedReadOnlyBackend:
136
+ """Read-only backend that merges two directories.
137
+
138
+ Reads from *primary* first (user skills in workspace/skills/),
139
+ falls back to *secondary* (system skills in ./skills/).
140
+ User skills override system skills with the same name.
141
+
142
+ Both directories share the same virtual path namespace — the agent
143
+ sees all skills under /skills/ regardless of which backend serves them.
144
+ """
145
+
146
+ def __init__(self, primary_dir: str, secondary_dir: str):
147
+ self._primary = ReadOnlyFilesystemBackend(root_dir=primary_dir, virtual_mode=True)
148
+ self._secondary = ReadOnlyFilesystemBackend(root_dir=secondary_dir, virtual_mode=True)
149
+
150
+ # -- read: try primary first, fall back to secondary --
151
+
152
+ def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
153
+ try:
154
+ result = self._primary.read(file_path, offset, limit)
155
+ if not result.startswith("Error:"):
156
+ return result
157
+ except (ValueError, FileNotFoundError, OSError):
158
+ pass
159
+ return self._secondary.read(file_path, offset, limit)
160
+
161
+ # -- ls_info: merge both, primary wins on name conflicts --
162
+
163
+ def ls_info(self, path: str = "/") -> list:
164
+ secondary_items = {item["path"]: item for item in self._secondary.ls_info(path)}
165
+ primary_items = {item["path"]: item for item in self._primary.ls_info(path)}
166
+ secondary_items.update(primary_items) # primary overrides
167
+ return sorted(secondary_items.values(), key=lambda x: x["path"])
168
+
169
+ # -- grep_raw: search both, deduplicate --
170
+
171
+ def grep_raw(self, pattern: str, path: str | None = None, glob: str | None = None) -> list:
172
+ results = self._secondary.grep_raw(pattern, path, glob)
173
+ try:
174
+ results += self._primary.grep_raw(pattern, path, glob)
175
+ except Exception:
176
+ pass
177
+ return results
178
+
179
+ # -- glob_info: merge both --
180
+
181
+ def glob_info(self, pattern: str, path: str = "/") -> list:
182
+ secondary = {item["path"]: item for item in self._secondary.glob_info(pattern, path)}
183
+ try:
184
+ primary = {item["path"]: item for item in self._primary.glob_info(pattern, path)}
185
+ secondary.update(primary)
186
+ except Exception:
187
+ pass
188
+ return sorted(secondary.values(), key=lambda x: x["path"])
189
+
190
+ # -- write / edit: blocked --
191
+
192
+ def write(self, file_path: str, content: str) -> WriteResult:
193
+ return WriteResult(
194
+ error="This directory is read-only. Write operations are not permitted here."
195
+ )
196
+
197
+ def edit(
198
+ self,
199
+ file_path: str,
200
+ old_string: str,
201
+ new_string: str,
202
+ replace_all: bool = False,
203
+ ) -> EditResult:
204
+ return EditResult(
205
+ error="This directory is read-only. Edit operations are not permitted here."
206
+ )
207
+
208
+ # -- async variants (required by middleware) --
209
+
210
+ async def aread(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
211
+ return self.read(file_path, offset, limit)
212
+
213
+ async def als_info(self, path: str = "/") -> list:
214
+ return self.ls_info(path)
215
+
216
+ async def agrep_raw(self, pattern: str, path: str | None = None, glob: str | None = None) -> list:
217
+ return self.grep_raw(pattern, path, glob)
218
+
219
+ async def aglob_info(self, pattern: str, path: str = "/") -> list:
220
+ return self.glob_info(pattern, path)
221
+
222
+ async def awrite(self, file_path: str, content: str) -> WriteResult:
223
+ return self.write(file_path, content)
224
+
225
+ async def aedit(self, file_path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult:
226
+ return self.edit(file_path, old_string, new_string, replace_all)
227
+
228
+ # -- download / upload (required by BackendProtocol) --
229
+
230
+ def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
231
+ """Download files, trying primary then secondary."""
232
+ responses: list[FileDownloadResponse] = []
233
+ for path in paths:
234
+ resp = self._primary.download_files([path])[0]
235
+ if resp.error is not None:
236
+ resp = self._secondary.download_files([path])[0]
237
+ responses.append(resp)
238
+ return responses
239
+
240
+ async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
241
+ return self.download_files(paths)
242
+
243
+ def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
244
+ return [
245
+ FileUploadResponse(path=path, error="permission_denied")
246
+ for path, _ in files
247
+ ]
248
+
249
+ async def aupload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
250
+ return self.upload_files(files)
251
+
252
+
253
+ class CustomSandboxBackend(FilesystemBackend, SandboxBackendProtocol):
254
+ """
255
+ Custom sandbox backend - inherits FilesystemBackend and implements execute method.
256
+
257
+ Features:
258
+ - Inherits all file operations (ls, read, write, edit, grep, glob)
259
+ - Adds shell command execution capability
260
+ - Command validation prevents directory traversal and dangerous operations
261
+ - Runs commands in specified working directory
262
+ - Compatible with LangGraph checkpointer (no thread locks)
263
+ """
264
+
265
+ def __init__(
266
+ self,
267
+ root_dir: str = ".",
268
+ virtual_mode: bool = True,
269
+ working_dir: str | None = None,
270
+ timeout: int = 300,
271
+ shell: str = "/bin/bash",
272
+ ):
273
+ """
274
+ Initialize custom sandbox backend.
275
+
276
+ Args:
277
+ root_dir: File system root directory
278
+ virtual_mode: Whether to enable virtual path mode
279
+ working_dir: Working directory for command execution (defaults to root_dir)
280
+ timeout: Command execution timeout in seconds
281
+ shell: Shell program to use
282
+ """
283
+ super().__init__(root_dir=root_dir, virtual_mode=virtual_mode)
284
+
285
+ self.working_dir = working_dir or root_dir
286
+ self.timeout = timeout
287
+ self.shell = shell
288
+ self.virtual_mode = virtual_mode
289
+
290
+ # Ensure working directory exists
291
+ os.makedirs(self.working_dir, exist_ok=True)
292
+
293
+ def _resolve_path(self, key: str) -> Path:
294
+ """Resolve path with sanitization to prevent nested directories.
295
+
296
+ Intercepts all file operations (read, write, edit, ls, grep, glob).
297
+ Auto-corrects common LLM path mistakes instead of crashing:
298
+ 1. /workspace/file.py → /file.py
299
+ 2. /Users/name/.../workspace/f → /f (strip up to workspace/)
300
+ 3. /Users/name/file.py → /file.py (keep basename)
301
+ """
302
+ # Auto-strip /workspace/ prefix to prevent nesting
303
+ if key.startswith("/workspace/"):
304
+ key = key[len("/workspace"):] # "/workspace/main.py" → "/main.py"
305
+ elif key == "/workspace":
306
+ key = "/"
307
+
308
+ # Auto-correct system absolute paths
309
+ for prefix in _SYSTEM_PATH_PREFIXES:
310
+ if key.startswith(prefix):
311
+ # Try to extract path after "workspace/" or "workspace" at end
312
+ marker = "/workspace/"
313
+ idx = key.find(marker)
314
+ if idx != -1:
315
+ key = "/" + key[idx + len(marker):]
316
+ elif key.endswith("/workspace"):
317
+ key = "/"
318
+ else:
319
+ # Fall back to basename
320
+ key = "/" + Path(key).name
321
+ break
322
+
323
+ return super()._resolve_path(key)
324
+
325
+ def execute(self, command: str) -> ExecuteResponse:
326
+ """
327
+ Execute shell command in sandbox environment.
328
+
329
+ Commands are validated before execution to prevent:
330
+ - Directory traversal (../)
331
+ - Access to paths outside workspace
332
+ - Dangerous system commands
333
+
334
+ Args:
335
+ command: Command string to execute
336
+
337
+ Returns:
338
+ ExecuteResponse containing output, exit_code, and truncated flag
339
+ """
340
+ try:
341
+ # Validate command safety
342
+ error = validate_command(command)
343
+ if error:
344
+ return ExecuteResponse(
345
+ output=error,
346
+ exit_code=1,
347
+ truncated=False,
348
+ )
349
+
350
+ # Convert virtual paths to relative paths
351
+ if self.virtual_mode:
352
+ command = convert_virtual_paths_in_command(command=command)
353
+
354
+ result = subprocess.run(
355
+ command,
356
+ shell=True,
357
+ executable=self.shell,
358
+ cwd=self.working_dir,
359
+ capture_output=True,
360
+ text=True,
361
+ timeout=self.timeout,
362
+ )
363
+
364
+ output = ""
365
+ if result.stdout:
366
+ output += result.stdout
367
+ if result.stderr:
368
+ output += result.stderr
369
+
370
+ return ExecuteResponse(
371
+ output=output,
372
+ exit_code=result.returncode,
373
+ truncated=False,
374
+ )
375
+
376
+ except subprocess.TimeoutExpired:
377
+ return ExecuteResponse(
378
+ output=f"Command timed out after {self.timeout} seconds",
379
+ exit_code=-1,
380
+ truncated=False,
381
+ )
382
+ except Exception as e:
383
+ return ExecuteResponse(
384
+ output=f"Error executing command: {str(e)}",
385
+ exit_code=-1,
386
+ truncated=False,
387
+ )
388
+
389
+ async def aexecute(self, command: str) -> ExecuteResponse:
390
+ """Async version of execute (runs sync version in thread)."""
391
+ import asyncio
392
+ return await asyncio.to_thread(self.execute, command)