superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,352 @@
1
+ """
2
+ Apply Patch Tool for Open Responses.
3
+
4
+ Implements the apply_patch built-in tool for applying unified diff patches
5
+ to files in the workspace. Critical for QIR (Quality Issue Resolution) fixes.
6
+
7
+ Features:
8
+ - Dry-run mode by default for safety
9
+ - Git-based patch application
10
+ - Validation before application
11
+ - Detailed error reporting
12
+
13
+ Usage:
14
+ tool = ApplyPatchTool(workspace_root="/path/to/project")
15
+
16
+ # Validate without applying
17
+ result = await tool.execute(patch_content, dry_run=True)
18
+
19
+ # Apply the patch
20
+ result = await tool.execute(patch_content, dry_run=False)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import os
27
+ import tempfile
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import Any, Dict, List, Optional
31
+
32
+
33
+ @dataclass
34
+ class PatchResult:
35
+ """Result of a patch operation."""
36
+
37
+ success: bool
38
+ message: str
39
+ files_modified: List[str] = field(default_factory=list)
40
+ errors: List[str] = field(default_factory=list)
41
+ dry_run: bool = True
42
+
43
+
44
+ @dataclass
45
+ class PatchOperation:
46
+ """A single file operation from a patch."""
47
+
48
+ operation: str # "create", "update", "delete"
49
+ path: str
50
+ old_path: Optional[str] = None # For renames
51
+
52
+
53
+ class ApplyPatchTool:
54
+ """
55
+ Apply patch tool for Open Responses.
56
+
57
+ Applies unified diff patches to files in the workspace.
58
+ Uses git apply for robust patch handling.
59
+
60
+ Args:
61
+ workspace_root: Root directory for file operations
62
+ dry_run: If True (default), validate without applying
63
+ allow_outside_workspace: If True, allow patches to files outside workspace
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ workspace_root: str,
69
+ dry_run: bool = True,
70
+ allow_outside_workspace: bool = False,
71
+ ):
72
+ self.workspace_root = Path(workspace_root).resolve()
73
+ self.dry_run = dry_run
74
+ self.allow_outside_workspace = allow_outside_workspace
75
+
76
+ async def execute(
77
+ self,
78
+ patch: str,
79
+ dry_run: Optional[bool] = None,
80
+ ) -> Dict[str, Any]:
81
+ """
82
+ Execute the patch operation.
83
+
84
+ Args:
85
+ patch: The patch content in unified diff format
86
+ dry_run: Override the default dry_run setting
87
+
88
+ Returns:
89
+ Dict with success status, message, and details
90
+ """
91
+ use_dry_run = dry_run if dry_run is not None else self.dry_run
92
+
93
+ # Parse patch to extract operations
94
+ operations = self._parse_patch(patch)
95
+
96
+ if not operations:
97
+ return {
98
+ "success": False,
99
+ "message": "No valid patch operations found",
100
+ "dry_run": use_dry_run,
101
+ }
102
+
103
+ # Validate paths
104
+ validation_errors = self._validate_paths(operations)
105
+ if validation_errors:
106
+ return {
107
+ "success": False,
108
+ "message": "Path validation failed",
109
+ "errors": validation_errors,
110
+ "dry_run": use_dry_run,
111
+ }
112
+
113
+ # Apply or validate the patch
114
+ if use_dry_run:
115
+ result = await self._validate_patch(patch)
116
+ else:
117
+ result = await self._apply_patch(patch)
118
+
119
+ return {
120
+ "success": result.success,
121
+ "message": result.message,
122
+ "files_modified": result.files_modified,
123
+ "errors": result.errors,
124
+ "dry_run": use_dry_run,
125
+ }
126
+
127
+ def _parse_patch(self, patch: str) -> List[PatchOperation]:
128
+ """Parse a patch to extract file operations."""
129
+ operations = []
130
+ lines = patch.split("\n")
131
+ current_file = None
132
+ is_new_file = False
133
+ is_delete = False
134
+
135
+ for line in lines:
136
+ if line.startswith("diff --git"):
137
+ # New file in patch
138
+ parts = line.split()
139
+ if len(parts) >= 4:
140
+ # Extract paths: diff --git a/path b/path
141
+ a_path = parts[2][2:] if parts[2].startswith("a/") else parts[2]
142
+ b_path = parts[3][2:] if parts[3].startswith("b/") else parts[3]
143
+ current_file = b_path if b_path != "/dev/null" else a_path
144
+ is_new_file = False
145
+ is_delete = False
146
+
147
+ elif line.startswith("new file mode"):
148
+ is_new_file = True
149
+
150
+ elif line.startswith("deleted file mode"):
151
+ is_delete = True
152
+
153
+ elif line.startswith("--- ") and current_file:
154
+ old_path = line[4:].strip()
155
+ if old_path.startswith("a/"):
156
+ old_path = old_path[2:]
157
+ elif old_path == "/dev/null":
158
+ is_new_file = True
159
+
160
+ elif line.startswith("+++ ") and current_file:
161
+ new_path = line[4:].strip()
162
+ if new_path.startswith("b/"):
163
+ new_path = new_path[2:]
164
+ elif new_path == "/dev/null":
165
+ is_delete = True
166
+
167
+ # Determine operation type
168
+ if is_delete:
169
+ op_type = "delete"
170
+ elif is_new_file:
171
+ op_type = "create"
172
+ else:
173
+ op_type = "update"
174
+
175
+ operations.append(
176
+ PatchOperation(
177
+ operation=op_type,
178
+ path=current_file,
179
+ )
180
+ )
181
+ current_file = None
182
+
183
+ return operations
184
+
185
+ def _validate_paths(self, operations: List[PatchOperation]) -> List[str]:
186
+ """Validate that all paths are within the workspace."""
187
+ errors = []
188
+
189
+ if self.allow_outside_workspace:
190
+ return errors
191
+
192
+ for op in operations:
193
+ try:
194
+ full_path = (self.workspace_root / op.path).resolve()
195
+ if not str(full_path).startswith(str(self.workspace_root)):
196
+ errors.append(f"Path '{op.path}' is outside workspace")
197
+ except Exception as e:
198
+ errors.append(f"Invalid path '{op.path}': {e}")
199
+
200
+ return errors
201
+
202
+ async def _validate_patch(self, patch: str) -> PatchResult:
203
+ """Validate a patch without applying it."""
204
+ # Write patch to temporary file
205
+ with tempfile.NamedTemporaryFile(
206
+ mode="w",
207
+ suffix=".patch",
208
+ delete=False,
209
+ ) as f:
210
+ f.write(patch)
211
+ patch_file = f.name
212
+
213
+ try:
214
+ # Run git apply --check
215
+ proc = await asyncio.create_subprocess_exec(
216
+ "git",
217
+ "apply",
218
+ "--check",
219
+ patch_file,
220
+ cwd=str(self.workspace_root),
221
+ stdout=asyncio.subprocess.PIPE,
222
+ stderr=asyncio.subprocess.PIPE,
223
+ )
224
+ stdout, stderr = await proc.communicate()
225
+
226
+ if proc.returncode == 0:
227
+ # Parse operations for file list
228
+ operations = self._parse_patch(patch)
229
+ return PatchResult(
230
+ success=True,
231
+ message="Patch validation successful",
232
+ files_modified=[op.path for op in operations],
233
+ dry_run=True,
234
+ )
235
+ else:
236
+ error_msg = stderr.decode("utf-8").strip()
237
+ return PatchResult(
238
+ success=False,
239
+ message="Patch validation failed",
240
+ errors=[error_msg] if error_msg else ["Patch does not apply cleanly"],
241
+ dry_run=True,
242
+ )
243
+
244
+ except FileNotFoundError:
245
+ # Git not available, try manual validation
246
+ return await self._validate_patch_manual(patch)
247
+ finally:
248
+ # Clean up temp file
249
+ try:
250
+ os.unlink(patch_file)
251
+ except Exception:
252
+ pass
253
+
254
+ async def _validate_patch_manual(self, patch: str) -> PatchResult:
255
+ """Manually validate a patch without git."""
256
+ operations = self._parse_patch(patch)
257
+ errors = []
258
+
259
+ for op in operations:
260
+ full_path = self.workspace_root / op.path
261
+
262
+ if op.operation == "update":
263
+ if not full_path.exists():
264
+ errors.append(f"File does not exist: {op.path}")
265
+
266
+ elif op.operation == "create":
267
+ if full_path.exists():
268
+ errors.append(f"File already exists: {op.path}")
269
+
270
+ elif op.operation == "delete":
271
+ if not full_path.exists():
272
+ errors.append(f"File does not exist: {op.path}")
273
+
274
+ if errors:
275
+ return PatchResult(
276
+ success=False,
277
+ message="Patch validation failed",
278
+ errors=errors,
279
+ dry_run=True,
280
+ )
281
+
282
+ return PatchResult(
283
+ success=True,
284
+ message="Patch validation successful (manual check)",
285
+ files_modified=[op.path for op in operations],
286
+ dry_run=True,
287
+ )
288
+
289
+ async def _apply_patch(self, patch: str) -> PatchResult:
290
+ """Apply a patch to the workspace."""
291
+ # Write patch to temporary file
292
+ with tempfile.NamedTemporaryFile(
293
+ mode="w",
294
+ suffix=".patch",
295
+ delete=False,
296
+ ) as f:
297
+ f.write(patch)
298
+ patch_file = f.name
299
+
300
+ try:
301
+ # Run git apply
302
+ proc = await asyncio.create_subprocess_exec(
303
+ "git",
304
+ "apply",
305
+ patch_file,
306
+ cwd=str(self.workspace_root),
307
+ stdout=asyncio.subprocess.PIPE,
308
+ stderr=asyncio.subprocess.PIPE,
309
+ )
310
+ stdout, stderr = await proc.communicate()
311
+
312
+ if proc.returncode == 0:
313
+ operations = self._parse_patch(patch)
314
+ return PatchResult(
315
+ success=True,
316
+ message="Patch applied successfully",
317
+ files_modified=[op.path for op in operations],
318
+ dry_run=False,
319
+ )
320
+ else:
321
+ error_msg = stderr.decode("utf-8").strip()
322
+ return PatchResult(
323
+ success=False,
324
+ message="Failed to apply patch",
325
+ errors=[error_msg] if error_msg else ["Patch application failed"],
326
+ dry_run=False,
327
+ )
328
+
329
+ except FileNotFoundError:
330
+ # Git not available, try manual application
331
+ return await self._apply_patch_manual(patch)
332
+ finally:
333
+ # Clean up temp file
334
+ try:
335
+ os.unlink(patch_file)
336
+ except Exception:
337
+ pass
338
+
339
+ async def _apply_patch_manual(self, patch: str) -> PatchResult:
340
+ """Manually apply a patch without git (limited support)."""
341
+ return PatchResult(
342
+ success=False,
343
+ message="Manual patch application not supported. Please install git.",
344
+ errors=["Git is required for patch application"],
345
+ dry_run=False,
346
+ )
347
+
348
+ def get_tool_definition(self) -> Dict[str, Any]:
349
+ """Get the Open Responses tool definition for apply_patch."""
350
+ return {
351
+ "type": "apply_patch",
352
+ }
@@ -0,0 +1,290 @@
1
+ """
2
+ Code Interpreter Tool for Open Responses.
3
+
4
+ Executes code in a sandboxed environment. Supports:
5
+ - Python execution
6
+ - Shell command execution
7
+ - Test running
8
+
9
+ Features:
10
+ - Timeout control
11
+ - Output capture
12
+ - Error handling
13
+ - Resource limits
14
+
15
+ Usage:
16
+ tool = CodeInterpreterTool(workspace_root="/path/to/project")
17
+ result = await tool.execute("print('Hello, World!')", language="python")
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import os
24
+ import tempfile
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Any, Dict, List, Optional
28
+
29
+
30
+ @dataclass
31
+ class ExecutionResult:
32
+ """Result of code execution."""
33
+
34
+ success: bool
35
+ output: str
36
+ error: str = ""
37
+ exit_code: int = 0
38
+ timed_out: bool = False
39
+
40
+
41
+ class CodeInterpreterTool:
42
+ """
43
+ Code interpreter tool for Open Responses.
44
+
45
+ Executes code in a controlled environment with timeout
46
+ and output capture.
47
+
48
+ Args:
49
+ workspace_root: Root directory for execution
50
+ timeout: Maximum execution time in seconds
51
+ max_output_size: Maximum output size in bytes
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ workspace_root: str,
57
+ timeout: float = 60.0,
58
+ max_output_size: int = 100 * 1024, # 100KB
59
+ ):
60
+ self.workspace_root = Path(workspace_root).resolve()
61
+ self.timeout = timeout
62
+ self.max_output_size = max_output_size
63
+
64
+ async def execute(
65
+ self,
66
+ code: str,
67
+ language: str = "python",
68
+ timeout: Optional[float] = None,
69
+ ) -> Dict[str, Any]:
70
+ """
71
+ Execute code in the specified language.
72
+
73
+ Args:
74
+ code: The code to execute
75
+ language: Programming language ("python", "shell", "bash")
76
+ timeout: Override default timeout
77
+
78
+ Returns:
79
+ Dict with success status, output, and details
80
+ """
81
+ use_timeout = timeout if timeout is not None else self.timeout
82
+
83
+ if language in ("python", "python3"):
84
+ result = await self._execute_python(code, use_timeout)
85
+ elif language in ("shell", "bash", "sh"):
86
+ result = await self._execute_shell(code, use_timeout)
87
+ else:
88
+ return {
89
+ "success": False,
90
+ "output": "",
91
+ "error": f"Unsupported language: {language}",
92
+ "exit_code": 1,
93
+ }
94
+
95
+ return {
96
+ "success": result.success,
97
+ "output": result.output,
98
+ "error": result.error,
99
+ "exit_code": result.exit_code,
100
+ "timed_out": result.timed_out,
101
+ }
102
+
103
+ async def run_tests(
104
+ self,
105
+ test_command: Optional[str] = None,
106
+ test_file: Optional[str] = None,
107
+ timeout: Optional[float] = None,
108
+ ) -> Dict[str, Any]:
109
+ """
110
+ Run tests in the workspace.
111
+
112
+ Args:
113
+ test_command: Custom test command
114
+ test_file: Specific test file to run
115
+ timeout: Override default timeout
116
+
117
+ Returns:
118
+ Dict with test results
119
+ """
120
+ use_timeout = timeout if timeout is not None else self.timeout
121
+
122
+ # Determine test command
123
+ if test_command:
124
+ cmd = test_command
125
+ elif test_file:
126
+ cmd = f"python -m pytest {test_file} -v"
127
+ else:
128
+ # Auto-detect test framework
129
+ cmd = await self._detect_test_command()
130
+
131
+ result = await self._execute_shell(cmd, use_timeout)
132
+
133
+ return {
134
+ "success": result.success,
135
+ "output": result.output,
136
+ "error": result.error,
137
+ "exit_code": result.exit_code,
138
+ "timed_out": result.timed_out,
139
+ "command": cmd,
140
+ }
141
+
142
+ async def _execute_python(
143
+ self,
144
+ code: str,
145
+ timeout: float,
146
+ ) -> ExecutionResult:
147
+ """Execute Python code."""
148
+ # Write code to temporary file
149
+ with tempfile.NamedTemporaryFile(
150
+ mode="w",
151
+ suffix=".py",
152
+ delete=False,
153
+ dir=str(self.workspace_root),
154
+ ) as f:
155
+ f.write(code)
156
+ script_file = f.name
157
+
158
+ try:
159
+ proc = await asyncio.create_subprocess_exec(
160
+ "python3",
161
+ script_file,
162
+ cwd=str(self.workspace_root),
163
+ stdout=asyncio.subprocess.PIPE,
164
+ stderr=asyncio.subprocess.PIPE,
165
+ env={**os.environ, "PYTHONUNBUFFERED": "1"},
166
+ )
167
+
168
+ try:
169
+ stdout, stderr = await asyncio.wait_for(
170
+ proc.communicate(),
171
+ timeout=timeout,
172
+ )
173
+ timed_out = False
174
+ except asyncio.TimeoutError:
175
+ proc.kill()
176
+ await proc.wait()
177
+ return ExecutionResult(
178
+ success=False,
179
+ output="",
180
+ error=f"Execution timed out after {timeout} seconds",
181
+ exit_code=-1,
182
+ timed_out=True,
183
+ )
184
+
185
+ output = stdout.decode("utf-8", errors="replace")
186
+ error = stderr.decode("utf-8", errors="replace")
187
+
188
+ # Truncate if needed
189
+ if len(output) > self.max_output_size:
190
+ output = output[: self.max_output_size] + "\n[Output truncated]"
191
+ if len(error) > self.max_output_size:
192
+ error = error[: self.max_output_size] + "\n[Error output truncated]"
193
+
194
+ return ExecutionResult(
195
+ success=proc.returncode == 0,
196
+ output=output,
197
+ error=error,
198
+ exit_code=proc.returncode or 0,
199
+ timed_out=False,
200
+ )
201
+
202
+ finally:
203
+ # Clean up temp file
204
+ try:
205
+ os.unlink(script_file)
206
+ except Exception:
207
+ pass
208
+
209
+ async def _execute_shell(
210
+ self,
211
+ command: str,
212
+ timeout: float,
213
+ ) -> ExecutionResult:
214
+ """Execute a shell command."""
215
+ try:
216
+ proc = await asyncio.create_subprocess_shell(
217
+ command,
218
+ cwd=str(self.workspace_root),
219
+ stdout=asyncio.subprocess.PIPE,
220
+ stderr=asyncio.subprocess.PIPE,
221
+ env=os.environ.copy(),
222
+ )
223
+
224
+ try:
225
+ stdout, stderr = await asyncio.wait_for(
226
+ proc.communicate(),
227
+ timeout=timeout,
228
+ )
229
+ timed_out = False
230
+ except asyncio.TimeoutError:
231
+ proc.kill()
232
+ await proc.wait()
233
+ return ExecutionResult(
234
+ success=False,
235
+ output="",
236
+ error=f"Execution timed out after {timeout} seconds",
237
+ exit_code=-1,
238
+ timed_out=True,
239
+ )
240
+
241
+ output = stdout.decode("utf-8", errors="replace")
242
+ error = stderr.decode("utf-8", errors="replace")
243
+
244
+ # Truncate if needed
245
+ if len(output) > self.max_output_size:
246
+ output = output[: self.max_output_size] + "\n[Output truncated]"
247
+ if len(error) > self.max_output_size:
248
+ error = error[: self.max_output_size] + "\n[Error output truncated]"
249
+
250
+ return ExecutionResult(
251
+ success=proc.returncode == 0,
252
+ output=output,
253
+ error=error,
254
+ exit_code=proc.returncode or 0,
255
+ timed_out=False,
256
+ )
257
+
258
+ except Exception as e:
259
+ return ExecutionResult(
260
+ success=False,
261
+ output="",
262
+ error=str(e),
263
+ exit_code=1,
264
+ timed_out=False,
265
+ )
266
+
267
+ async def _detect_test_command(self) -> str:
268
+ """Detect the appropriate test command for the workspace."""
269
+ # Check for common test configurations
270
+ if (self.workspace_root / "pytest.ini").exists():
271
+ return "python -m pytest -v"
272
+ if (self.workspace_root / "pyproject.toml").exists():
273
+ return "python -m pytest -v"
274
+ if (self.workspace_root / "setup.py").exists():
275
+ return "python -m pytest -v"
276
+ if (self.workspace_root / "package.json").exists():
277
+ return "npm test"
278
+ if (self.workspace_root / "Cargo.toml").exists():
279
+ return "cargo test"
280
+ if (self.workspace_root / "go.mod").exists():
281
+ return "go test ./..."
282
+
283
+ # Default to pytest
284
+ return "python -m pytest -v"
285
+
286
+ def get_tool_definition(self) -> Dict[str, Any]:
287
+ """Get the Open Responses tool definition for code_interpreter."""
288
+ return {
289
+ "type": "code_interpreter",
290
+ }