acontext 0.1.4__py3-none-any.whl → 0.1.6__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.
- acontext/agent/prompts.py +4 -1
- acontext/agent/sandbox.py +26 -10
- acontext/agent/text_editor.py +199 -215
- {acontext-0.1.4.dist-info → acontext-0.1.6.dist-info}/METADATA +1 -1
- {acontext-0.1.4.dist-info → acontext-0.1.6.dist-info}/RECORD +6 -6
- {acontext-0.1.4.dist-info → acontext-0.1.6.dist-info}/WHEEL +1 -1
acontext/agent/prompts.py
CHANGED
|
@@ -92,5 +92,8 @@ Container environment:
|
|
|
92
92
|
- Standard Unix utilities available (grep, sed, awk, etc.)
|
|
93
93
|
- Archive tools: tar, unzip, zip
|
|
94
94
|
- Additional tools: ripgrep, fd, sqlite3, jq, imagemagick
|
|
95
|
-
-
|
|
95
|
+
- You can install new packages with pip if needed (internet access is available)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
Remember to always export your artifacts at the end of your task so that the user can view them.
|
|
96
99
|
"""
|
acontext/agent/sandbox.py
CHANGED
|
@@ -10,6 +10,15 @@ from .prompts import SANDBOX_TEXT_EDITOR_REMINDER, SANDBOX_BASH_REMINDER, SKILL_
|
|
|
10
10
|
from ..client import AcontextClient
|
|
11
11
|
from ..async_client import AcontextAsyncClient
|
|
12
12
|
|
|
13
|
+
MAX_OUTPUT_CHARS = 20000
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def truncate_output(text: str, max_chars: int = MAX_OUTPUT_CHARS) -> str:
|
|
17
|
+
"""Truncate text to max_chars, appending a truncation flag if needed."""
|
|
18
|
+
if len(text) > max_chars:
|
|
19
|
+
return text[:max_chars] + "...[truncated]"
|
|
20
|
+
return text
|
|
21
|
+
|
|
13
22
|
|
|
14
23
|
class MountedSkill(TypedDict):
|
|
15
24
|
name: str
|
|
@@ -191,8 +200,8 @@ class BashTool(BaseTool):
|
|
|
191
200
|
|
|
192
201
|
return json.dumps(
|
|
193
202
|
{
|
|
194
|
-
"stdout": result.stdout,
|
|
195
|
-
"stderr": result.stderr,
|
|
203
|
+
"stdout": truncate_output(result.stdout),
|
|
204
|
+
"stderr": truncate_output(result.stderr),
|
|
196
205
|
"exit_code": result.exit_code,
|
|
197
206
|
}
|
|
198
207
|
)
|
|
@@ -213,8 +222,8 @@ class BashTool(BaseTool):
|
|
|
213
222
|
|
|
214
223
|
return json.dumps(
|
|
215
224
|
{
|
|
216
|
-
"stdout": result.stdout,
|
|
217
|
-
"stderr": result.stderr,
|
|
225
|
+
"stdout": truncate_output(result.stdout),
|
|
226
|
+
"stderr": truncate_output(result.stderr),
|
|
218
227
|
"exit_code": result.exit_code,
|
|
219
228
|
}
|
|
220
229
|
)
|
|
@@ -248,27 +257,34 @@ class TextEditorTool(BaseTool):
|
|
|
248
257
|
"command": {
|
|
249
258
|
"type": "string",
|
|
250
259
|
"enum": ["view", "create", "str_replace"],
|
|
251
|
-
"description":
|
|
260
|
+
"description": (
|
|
261
|
+
"Perform only text operations: 'view', 'create', or 'str_replace'. "
|
|
262
|
+
"Required parameters per command: "
|
|
263
|
+
"'view' requires path (view_range is optional); "
|
|
264
|
+
"'create' requires path and file_text; "
|
|
265
|
+
"'str_replace' requires path, old_str, and new_str."
|
|
266
|
+
),
|
|
252
267
|
},
|
|
253
268
|
"path": {
|
|
254
269
|
"type": "string",
|
|
255
|
-
"description": "The file path in the sandbox (e.g., '/workspace/script.py')",
|
|
270
|
+
"description": "Required for all commands. The file path in the sandbox (e.g., '/workspace/script.py')",
|
|
256
271
|
},
|
|
257
272
|
"file_text": {
|
|
258
273
|
"type": ["string", "null"],
|
|
259
|
-
"description": "
|
|
274
|
+
"description": "Required for 'create' command. The content to write to the file.",
|
|
260
275
|
},
|
|
261
276
|
"old_str": {
|
|
262
277
|
"type": ["string", "null"],
|
|
263
|
-
"description": "
|
|
278
|
+
"description": "Required for 'str_replace' command. The exact string to find and replace.",
|
|
264
279
|
},
|
|
265
280
|
"new_str": {
|
|
266
281
|
"type": ["string", "null"],
|
|
267
|
-
"description": "
|
|
282
|
+
"description": "Required for 'str_replace' command. The string to replace old_str with.",
|
|
268
283
|
},
|
|
269
284
|
"view_range": {
|
|
270
285
|
"type": ["array", "null"],
|
|
271
|
-
"
|
|
286
|
+
"items": {"type": "integer"},
|
|
287
|
+
"description": "Optional for 'view' command. An array [start_line, end_line] to view specific lines. If not provided, shows the first 200 lines.",
|
|
272
288
|
},
|
|
273
289
|
}
|
|
274
290
|
|
acontext/agent/text_editor.py
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
"""Text editor file operations for sandbox environments."""
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
|
+
import posixpath
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
if TYPE_CHECKING:
|
|
7
8
|
from .sandbox import AsyncSandboxContext, SandboxContext
|
|
8
9
|
|
|
10
|
+
MAX_CONTENT_CHARS = 20000
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def truncate_content(text: str, max_chars: int = MAX_CONTENT_CHARS) -> str:
|
|
14
|
+
"""Truncate text to max_chars, appending a truncation flag if needed."""
|
|
15
|
+
if len(text) > max_chars:
|
|
16
|
+
return text[:max_chars] + "...[truncated]"
|
|
17
|
+
return text
|
|
18
|
+
|
|
9
19
|
|
|
10
20
|
def escape_for_shell(s: str) -> str:
|
|
11
21
|
"""Escape a string for safe use in shell commands."""
|
|
@@ -32,51 +42,53 @@ def view_file(
|
|
|
32
42
|
Returns:
|
|
33
43
|
A dict with file content and metadata, or error information.
|
|
34
44
|
"""
|
|
35
|
-
|
|
36
|
-
check_cmd = f"wc -l < {escape_for_shell(path)} 2>/dev/null || echo 'FILE_NOT_FOUND'"
|
|
37
|
-
result = ctx.client.sandboxes.exec_command(
|
|
38
|
-
sandbox_id=ctx.sandbox_id,
|
|
39
|
-
command=check_cmd,
|
|
40
|
-
timeout=timeout,
|
|
41
|
-
)
|
|
45
|
+
escaped_path = escape_for_shell(path)
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
"error": f"File not found: {path}",
|
|
46
|
-
"stderr": result.stderr,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
total_lines = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
|
|
50
|
-
|
|
51
|
-
# Build the view command with line numbers
|
|
47
|
+
# Build combined command: check existence, get total lines, and view content in one exec
|
|
52
48
|
if view_range and len(view_range) == 2:
|
|
53
49
|
start_line, end_line = view_range
|
|
54
|
-
|
|
50
|
+
view_cmd = (
|
|
51
|
+
f"sed -n '{start_line},{end_line}p' {escaped_path} | nl -ba -v {start_line}"
|
|
52
|
+
)
|
|
55
53
|
else:
|
|
56
|
-
|
|
54
|
+
max_lines = 200
|
|
55
|
+
view_cmd = f"head -n {max_lines} {escaped_path} | nl -ba"
|
|
57
56
|
start_line = 1
|
|
58
57
|
|
|
58
|
+
# Single combined command: outputs "TOTAL:<n>" on first line, then file content
|
|
59
|
+
cmd = (
|
|
60
|
+
f"if [ ! -f {escaped_path} ]; then echo 'FILE_NOT_FOUND'; exit 1; fi; "
|
|
61
|
+
f'echo "TOTAL:$(wc -l < {escaped_path})"; {view_cmd}'
|
|
62
|
+
)
|
|
63
|
+
|
|
59
64
|
result = ctx.client.sandboxes.exec_command(
|
|
60
65
|
sandbox_id=ctx.sandbox_id,
|
|
61
66
|
command=cmd,
|
|
62
67
|
timeout=timeout,
|
|
63
68
|
)
|
|
64
69
|
|
|
65
|
-
if result.exit_code != 0:
|
|
70
|
+
if result.exit_code != 0 or "FILE_NOT_FOUND" in result.stdout:
|
|
66
71
|
return {
|
|
67
|
-
"error": f"
|
|
72
|
+
"error": f"File not found: {path}",
|
|
68
73
|
"stderr": result.stderr,
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
# Parse output: first line is "TOTAL:<n>", rest is content
|
|
77
|
+
lines = result.stdout.split("\n", 1)
|
|
78
|
+
total_lines = 0
|
|
79
|
+
content = ""
|
|
80
|
+
|
|
81
|
+
if lines and lines[0].startswith("TOTAL:"):
|
|
82
|
+
total_str = lines[0][6:].strip()
|
|
83
|
+
total_lines = int(total_str) if total_str.isdigit() else 0
|
|
84
|
+
content = lines[1] if len(lines) > 1 else ""
|
|
85
|
+
|
|
86
|
+
content_lines = content.rstrip("\n").split("\n") if content.strip() else []
|
|
75
87
|
num_lines = len(content_lines)
|
|
76
88
|
|
|
77
89
|
return {
|
|
78
90
|
"file_type": "text",
|
|
79
|
-
"content":
|
|
91
|
+
"content": truncate_content(content),
|
|
80
92
|
"numLines": num_lines,
|
|
81
93
|
"startLine": start_line if view_range else 1,
|
|
82
94
|
"totalLines": total_lines + 1, # wc -l doesn't count last line without newline
|
|
@@ -97,41 +109,35 @@ def create_file(
|
|
|
97
109
|
Returns:
|
|
98
110
|
A dict with creation status or error information.
|
|
99
111
|
"""
|
|
100
|
-
|
|
101
|
-
check_cmd = f"test -f {escape_for_shell(path)} && echo 'EXISTS' || echo 'NEW'"
|
|
102
|
-
check_result = ctx.client.sandboxes.exec_command(
|
|
103
|
-
sandbox_id=ctx.sandbox_id,
|
|
104
|
-
command=check_cmd,
|
|
105
|
-
timeout=timeout,
|
|
106
|
-
)
|
|
107
|
-
is_update = "EXISTS" in check_result.stdout
|
|
108
|
-
|
|
109
|
-
# Create directory if needed
|
|
110
|
-
dir_path = "/".join(path.split("/")[:-1])
|
|
111
|
-
if dir_path:
|
|
112
|
-
mkdir_cmd = f"mkdir -p {escape_for_shell(dir_path)}"
|
|
113
|
-
ctx.client.sandboxes.exec_command(
|
|
114
|
-
sandbox_id=ctx.sandbox_id,
|
|
115
|
-
command=mkdir_cmd,
|
|
116
|
-
timeout=timeout,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# Write file using base64 encoding to safely transfer content
|
|
112
|
+
escaped_path = escape_for_shell(path)
|
|
120
113
|
encoded_content = base64.b64encode(file_text.encode()).decode()
|
|
121
|
-
|
|
114
|
+
|
|
115
|
+
# Get directory path for mkdir
|
|
116
|
+
dir_path = posixpath.dirname(path)
|
|
117
|
+
mkdir_part = f"mkdir -p {escape_for_shell(dir_path)} && " if dir_path else ""
|
|
118
|
+
|
|
119
|
+
# Single combined command: check existence, create dir, write file
|
|
120
|
+
cmd = (
|
|
121
|
+
f"is_update=$(test -f {escaped_path} && echo 1 || echo 0); "
|
|
122
|
+
f"{mkdir_part}"
|
|
123
|
+
f"echo {escape_for_shell(encoded_content)} | base64 -d > {escaped_path} && "
|
|
124
|
+
f'echo "STATUS:$is_update"'
|
|
125
|
+
)
|
|
122
126
|
|
|
123
127
|
result = ctx.client.sandboxes.exec_command(
|
|
124
128
|
sandbox_id=ctx.sandbox_id,
|
|
125
|
-
command=
|
|
129
|
+
command=cmd,
|
|
126
130
|
timeout=timeout,
|
|
127
131
|
)
|
|
128
132
|
|
|
129
|
-
if result.exit_code != 0:
|
|
133
|
+
if result.exit_code != 0 or "STATUS:" not in result.stdout:
|
|
130
134
|
return {
|
|
131
135
|
"error": f"Failed to create file: {path}",
|
|
132
136
|
"stderr": result.stderr,
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
is_update = "STATUS:1" in result.stdout
|
|
140
|
+
|
|
135
141
|
return {
|
|
136
142
|
"is_file_update": is_update,
|
|
137
143
|
"message": f"File {'updated' if is_update else 'created'}: {path}",
|
|
@@ -143,6 +149,9 @@ def str_replace(
|
|
|
143
149
|
) -> dict:
|
|
144
150
|
"""Replace a string in a file.
|
|
145
151
|
|
|
152
|
+
Uses a Python script on the sandbox to avoid transferring the entire file.
|
|
153
|
+
Only the base64-encoded old_str and new_str are sent.
|
|
154
|
+
|
|
146
155
|
Args:
|
|
147
156
|
ctx: The sandbox context.
|
|
148
157
|
path: The file path to modify.
|
|
@@ -151,85 +160,62 @@ def str_replace(
|
|
|
151
160
|
timeout: Optional timeout for command execution.
|
|
152
161
|
|
|
153
162
|
Returns:
|
|
154
|
-
A dict with
|
|
163
|
+
A dict with success message or error details.
|
|
155
164
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
old_b64 = base64.b64encode(old_str.encode()).decode()
|
|
166
|
+
new_b64 = base64.b64encode(new_str.encode()).decode()
|
|
167
|
+
|
|
168
|
+
# Write Python script to a temp file and execute it
|
|
169
|
+
# This avoids shell escaping issues with inline python -c
|
|
170
|
+
py_script = f'''import sys, base64, os
|
|
171
|
+
old = base64.b64decode("{old_b64}").decode()
|
|
172
|
+
new = base64.b64decode("{new_b64}").decode()
|
|
173
|
+
path = "{path}"
|
|
174
|
+
if not os.path.exists(path):
|
|
175
|
+
print("FILE_NOT_FOUND")
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
with open(path, "r") as f:
|
|
178
|
+
content = f.read()
|
|
179
|
+
count = content.count(old)
|
|
180
|
+
if count == 0:
|
|
181
|
+
print("NOT_FOUND")
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
if count > 1:
|
|
184
|
+
print(f"MULTIPLE:{{count}}")
|
|
185
|
+
sys.exit(0)
|
|
186
|
+
with open(path, "w") as f:
|
|
187
|
+
f.write(content.replace(old, new, 1))
|
|
188
|
+
print("SUCCESS")
|
|
189
|
+
'''
|
|
190
|
+
# Base64 encode the script itself to avoid any escaping issues
|
|
191
|
+
script_b64 = base64.b64encode(py_script.encode()).decode()
|
|
192
|
+
cmd = f"echo {escape_for_shell(script_b64)} | base64 -d | python3"
|
|
193
|
+
|
|
158
194
|
result = ctx.client.sandboxes.exec_command(
|
|
159
195
|
sandbox_id=ctx.sandbox_id,
|
|
160
|
-
command=
|
|
196
|
+
command=cmd,
|
|
161
197
|
timeout=timeout,
|
|
162
198
|
)
|
|
163
199
|
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
"error": f"File not found: {path}",
|
|
167
|
-
"stderr": result.stderr,
|
|
168
|
-
}
|
|
200
|
+
output = result.stdout.strip()
|
|
169
201
|
|
|
170
|
-
|
|
202
|
+
if result.exit_code != 0 or output == "FILE_NOT_FOUND":
|
|
203
|
+
return {"error": f"File not found: {path}", "stderr": result.stderr}
|
|
171
204
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
"error": f"String not found in file: {old_str[:50]}...",
|
|
176
|
-
}
|
|
205
|
+
if output == "NOT_FOUND":
|
|
206
|
+
return {"error": f"String not found in file: {old_str[:50]}..."}
|
|
177
207
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if occurrences > 1:
|
|
208
|
+
if output.startswith("MULTIPLE:"):
|
|
209
|
+
count = output.split(":")[1]
|
|
181
210
|
return {
|
|
182
|
-
"error": f"Multiple occurrences ({
|
|
211
|
+
"error": f"Multiple occurrences ({count}) of the string found. "
|
|
212
|
+
"Please provide more context to make the match unique."
|
|
183
213
|
}
|
|
184
214
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
# Find the line numbers affected
|
|
189
|
-
old_lines = original_content.split("\n")
|
|
190
|
-
new_lines = new_content.split("\n")
|
|
191
|
-
|
|
192
|
-
# Find where the change starts
|
|
193
|
-
old_start = 1
|
|
194
|
-
for i, (old_line, new_line) in enumerate(zip(old_lines, new_lines)):
|
|
195
|
-
if old_line != new_line:
|
|
196
|
-
old_start = i + 1
|
|
197
|
-
break
|
|
215
|
+
if output == "SUCCESS":
|
|
216
|
+
return {"msg": "Successfully replaced text at exactly one location."}
|
|
198
217
|
|
|
199
|
-
|
|
200
|
-
encoded_content = base64.b64encode(new_content.encode()).decode()
|
|
201
|
-
write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
|
|
202
|
-
|
|
203
|
-
result = ctx.client.sandboxes.exec_command(
|
|
204
|
-
sandbox_id=ctx.sandbox_id,
|
|
205
|
-
command=write_cmd,
|
|
206
|
-
timeout=timeout,
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
if result.exit_code != 0:
|
|
210
|
-
return {
|
|
211
|
-
"error": f"Failed to write file: {path}",
|
|
212
|
-
"stderr": result.stderr,
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
# Calculate diff info
|
|
216
|
-
old_str_lines = old_str.count("\n") + 1
|
|
217
|
-
new_str_lines = new_str.count("\n") + 1
|
|
218
|
-
|
|
219
|
-
# Build diff lines
|
|
220
|
-
diff_lines = []
|
|
221
|
-
for line in old_str.split("\n"):
|
|
222
|
-
diff_lines.append(f"-{line}")
|
|
223
|
-
for line in new_str.split("\n"):
|
|
224
|
-
diff_lines.append(f"+{line}")
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
"oldStart": old_start,
|
|
228
|
-
"oldLines": old_str_lines,
|
|
229
|
-
"newStart": old_start,
|
|
230
|
-
"newLines": new_str_lines,
|
|
231
|
-
"lines": diff_lines,
|
|
232
|
-
}
|
|
218
|
+
return {"error": f"Unexpected response: {output}", "stderr": result.stderr}
|
|
233
219
|
|
|
234
220
|
|
|
235
221
|
# ============================================================================
|
|
@@ -238,7 +224,10 @@ def str_replace(
|
|
|
238
224
|
|
|
239
225
|
|
|
240
226
|
async def async_view_file(
|
|
241
|
-
ctx: "AsyncSandboxContext",
|
|
227
|
+
ctx: "AsyncSandboxContext",
|
|
228
|
+
path: str,
|
|
229
|
+
view_range: list | None,
|
|
230
|
+
timeout: float | None,
|
|
242
231
|
) -> dict:
|
|
243
232
|
"""View file content with line numbers (async).
|
|
244
233
|
|
|
@@ -251,48 +240,53 @@ async def async_view_file(
|
|
|
251
240
|
Returns:
|
|
252
241
|
A dict with file content and metadata, or error information.
|
|
253
242
|
"""
|
|
254
|
-
|
|
255
|
-
result = await ctx.client.sandboxes.exec_command(
|
|
256
|
-
sandbox_id=ctx.sandbox_id,
|
|
257
|
-
command=check_cmd,
|
|
258
|
-
timeout=timeout,
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
if "FILE_NOT_FOUND" in result.stdout or result.exit_code != 0:
|
|
262
|
-
return {
|
|
263
|
-
"error": f"File not found: {path}",
|
|
264
|
-
"stderr": result.stderr,
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
total_lines = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
|
|
243
|
+
escaped_path = escape_for_shell(path)
|
|
268
244
|
|
|
245
|
+
# Build combined command: check existence, get total lines, and view content in one exec
|
|
269
246
|
if view_range and len(view_range) == 2:
|
|
270
247
|
start_line, end_line = view_range
|
|
271
|
-
|
|
248
|
+
view_cmd = (
|
|
249
|
+
f"sed -n '{start_line},{end_line}p' {escaped_path} | nl -ba -v {start_line}"
|
|
250
|
+
)
|
|
272
251
|
else:
|
|
273
|
-
|
|
252
|
+
max_lines = 200
|
|
253
|
+
view_cmd = f"head -n {max_lines} {escaped_path} | nl -ba"
|
|
274
254
|
start_line = 1
|
|
275
255
|
|
|
256
|
+
# Single combined command: outputs "TOTAL:<n>" on first line, then file content
|
|
257
|
+
cmd = (
|
|
258
|
+
f"if [ ! -f {escaped_path} ]; then echo 'FILE_NOT_FOUND'; exit 1; fi; "
|
|
259
|
+
f'echo "TOTAL:$(wc -l < {escaped_path})"; {view_cmd}'
|
|
260
|
+
)
|
|
261
|
+
|
|
276
262
|
result = await ctx.client.sandboxes.exec_command(
|
|
277
263
|
sandbox_id=ctx.sandbox_id,
|
|
278
264
|
command=cmd,
|
|
279
265
|
timeout=timeout,
|
|
280
266
|
)
|
|
281
267
|
|
|
282
|
-
if result.exit_code != 0:
|
|
268
|
+
if result.exit_code != 0 or "FILE_NOT_FOUND" in result.stdout:
|
|
283
269
|
return {
|
|
284
|
-
"error": f"
|
|
270
|
+
"error": f"File not found: {path}",
|
|
285
271
|
"stderr": result.stderr,
|
|
286
272
|
}
|
|
287
273
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
274
|
+
# Parse output: first line is "TOTAL:<n>", rest is content
|
|
275
|
+
lines = result.stdout.split("\n", 1)
|
|
276
|
+
total_lines = 0
|
|
277
|
+
content = ""
|
|
278
|
+
|
|
279
|
+
if lines and lines[0].startswith("TOTAL:"):
|
|
280
|
+
total_str = lines[0][6:].strip()
|
|
281
|
+
total_lines = int(total_str) if total_str.isdigit() else 0
|
|
282
|
+
content = lines[1] if len(lines) > 1 else ""
|
|
283
|
+
|
|
284
|
+
content_lines = content.rstrip("\n").split("\n") if content.strip() else []
|
|
291
285
|
num_lines = len(content_lines)
|
|
292
286
|
|
|
293
287
|
return {
|
|
294
288
|
"file_type": "text",
|
|
295
|
-
"content":
|
|
289
|
+
"content": truncate_content(content),
|
|
296
290
|
"numLines": num_lines,
|
|
297
291
|
"startLine": start_line if view_range else 1,
|
|
298
292
|
"totalLines": total_lines + 1,
|
|
@@ -313,38 +307,35 @@ async def async_create_file(
|
|
|
313
307
|
Returns:
|
|
314
308
|
A dict with creation status or error information.
|
|
315
309
|
"""
|
|
316
|
-
|
|
317
|
-
check_result = await ctx.client.sandboxes.exec_command(
|
|
318
|
-
sandbox_id=ctx.sandbox_id,
|
|
319
|
-
command=check_cmd,
|
|
320
|
-
timeout=timeout,
|
|
321
|
-
)
|
|
322
|
-
is_update = "EXISTS" in check_result.stdout
|
|
323
|
-
|
|
324
|
-
dir_path = "/".join(path.split("/")[:-1])
|
|
325
|
-
if dir_path:
|
|
326
|
-
mkdir_cmd = f"mkdir -p {escape_for_shell(dir_path)}"
|
|
327
|
-
await ctx.client.sandboxes.exec_command(
|
|
328
|
-
sandbox_id=ctx.sandbox_id,
|
|
329
|
-
command=mkdir_cmd,
|
|
330
|
-
timeout=timeout,
|
|
331
|
-
)
|
|
332
|
-
|
|
310
|
+
escaped_path = escape_for_shell(path)
|
|
333
311
|
encoded_content = base64.b64encode(file_text.encode()).decode()
|
|
334
|
-
|
|
312
|
+
|
|
313
|
+
# Get directory path for mkdir
|
|
314
|
+
dir_path = posixpath.dirname(path)
|
|
315
|
+
mkdir_part = f"mkdir -p {escape_for_shell(dir_path)} && " if dir_path else ""
|
|
316
|
+
|
|
317
|
+
# Single combined command: check existence, create dir, write file
|
|
318
|
+
cmd = (
|
|
319
|
+
f"is_update=$(test -f {escaped_path} && echo 1 || echo 0); "
|
|
320
|
+
f"{mkdir_part}"
|
|
321
|
+
f"echo {escape_for_shell(encoded_content)} | base64 -d > {escaped_path} && "
|
|
322
|
+
f'echo "STATUS:$is_update"'
|
|
323
|
+
)
|
|
335
324
|
|
|
336
325
|
result = await ctx.client.sandboxes.exec_command(
|
|
337
326
|
sandbox_id=ctx.sandbox_id,
|
|
338
|
-
command=
|
|
327
|
+
command=cmd,
|
|
339
328
|
timeout=timeout,
|
|
340
329
|
)
|
|
341
330
|
|
|
342
|
-
if result.exit_code != 0:
|
|
331
|
+
if result.exit_code != 0 or "STATUS:" not in result.stdout:
|
|
343
332
|
return {
|
|
344
333
|
"error": f"Failed to create file: {path}",
|
|
345
334
|
"stderr": result.stderr,
|
|
346
335
|
}
|
|
347
336
|
|
|
337
|
+
is_update = "STATUS:1" in result.stdout
|
|
338
|
+
|
|
348
339
|
return {
|
|
349
340
|
"is_file_update": is_update,
|
|
350
341
|
"message": f"File {'updated' if is_update else 'created'}: {path}",
|
|
@@ -352,10 +343,17 @@ async def async_create_file(
|
|
|
352
343
|
|
|
353
344
|
|
|
354
345
|
async def async_str_replace(
|
|
355
|
-
ctx: "AsyncSandboxContext",
|
|
346
|
+
ctx: "AsyncSandboxContext",
|
|
347
|
+
path: str,
|
|
348
|
+
old_str: str,
|
|
349
|
+
new_str: str,
|
|
350
|
+
timeout: float | None,
|
|
356
351
|
) -> dict:
|
|
357
352
|
"""Replace a string in a file (async).
|
|
358
353
|
|
|
354
|
+
Uses a Python script on the sandbox to avoid transferring the entire file.
|
|
355
|
+
Only the base64-encoded old_str and new_str are sent.
|
|
356
|
+
|
|
359
357
|
Args:
|
|
360
358
|
ctx: The async sandbox context.
|
|
361
359
|
path: The file path to modify.
|
|
@@ -364,73 +362,59 @@ async def async_str_replace(
|
|
|
364
362
|
timeout: Optional timeout for command execution.
|
|
365
363
|
|
|
366
364
|
Returns:
|
|
367
|
-
A dict with
|
|
365
|
+
A dict with success message or error details.
|
|
368
366
|
"""
|
|
369
|
-
|
|
367
|
+
old_b64 = base64.b64encode(old_str.encode()).decode()
|
|
368
|
+
new_b64 = base64.b64encode(new_str.encode()).decode()
|
|
369
|
+
|
|
370
|
+
# Write Python script to a temp file and execute it
|
|
371
|
+
# This avoids shell escaping issues with inline python -c
|
|
372
|
+
py_script = f'''import sys, base64, os
|
|
373
|
+
old = base64.b64decode("{old_b64}").decode()
|
|
374
|
+
new = base64.b64decode("{new_b64}").decode()
|
|
375
|
+
path = "{path}"
|
|
376
|
+
if not os.path.exists(path):
|
|
377
|
+
print("FILE_NOT_FOUND")
|
|
378
|
+
sys.exit(1)
|
|
379
|
+
with open(path, "r") as f:
|
|
380
|
+
content = f.read()
|
|
381
|
+
count = content.count(old)
|
|
382
|
+
if count == 0:
|
|
383
|
+
print("NOT_FOUND")
|
|
384
|
+
sys.exit(0)
|
|
385
|
+
if count > 1:
|
|
386
|
+
print(f"MULTIPLE:{{count}}")
|
|
387
|
+
sys.exit(0)
|
|
388
|
+
with open(path, "w") as f:
|
|
389
|
+
f.write(content.replace(old, new, 1))
|
|
390
|
+
print("SUCCESS")
|
|
391
|
+
'''
|
|
392
|
+
# Base64 encode the script itself to avoid any escaping issues
|
|
393
|
+
script_b64 = base64.b64encode(py_script.encode()).decode()
|
|
394
|
+
cmd = f"echo {escape_for_shell(script_b64)} | base64 -d | python3"
|
|
395
|
+
|
|
370
396
|
result = await ctx.client.sandboxes.exec_command(
|
|
371
397
|
sandbox_id=ctx.sandbox_id,
|
|
372
|
-
command=
|
|
398
|
+
command=cmd,
|
|
373
399
|
timeout=timeout,
|
|
374
400
|
)
|
|
375
401
|
|
|
376
|
-
|
|
377
|
-
return {
|
|
378
|
-
"error": f"File not found: {path}",
|
|
379
|
-
"stderr": result.stderr,
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
original_content = result.stdout
|
|
383
|
-
|
|
384
|
-
if old_str not in original_content:
|
|
385
|
-
return {
|
|
386
|
-
"error": f"String not found in file: {old_str[:50]}...",
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
occurrences = original_content.count(old_str)
|
|
390
|
-
if occurrences > 1:
|
|
391
|
-
return {
|
|
392
|
-
"error": f"Multiple occurrences ({occurrences}) of the string found. Please provide more context to make the match unique.",
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
new_content = original_content.replace(old_str, new_str, 1)
|
|
396
|
-
|
|
397
|
-
old_lines = original_content.split("\n")
|
|
398
|
-
new_lines = new_content.split("\n")
|
|
402
|
+
output = result.stdout.strip()
|
|
399
403
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if old_line != new_line:
|
|
403
|
-
old_start = i + 1
|
|
404
|
-
break
|
|
404
|
+
if result.exit_code != 0 or output == "FILE_NOT_FOUND":
|
|
405
|
+
return {"error": f"File not found: {path}", "stderr": result.stderr}
|
|
405
406
|
|
|
406
|
-
|
|
407
|
-
|
|
407
|
+
if output == "NOT_FOUND":
|
|
408
|
+
return {"error": f"String not found in file: {old_str[:50]}..."}
|
|
408
409
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
command=write_cmd,
|
|
412
|
-
timeout=timeout,
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
if result.exit_code != 0:
|
|
410
|
+
if output.startswith("MULTIPLE:"):
|
|
411
|
+
count = output.split(":")[1]
|
|
416
412
|
return {
|
|
417
|
-
"error": f"
|
|
418
|
-
"
|
|
413
|
+
"error": f"Multiple occurrences ({count}) of the string found. "
|
|
414
|
+
"Please provide more context to make the match unique."
|
|
419
415
|
}
|
|
420
416
|
|
|
421
|
-
|
|
422
|
-
|
|
417
|
+
if output == "SUCCESS":
|
|
418
|
+
return {"msg": "Successfully replaced text at exactly one location."}
|
|
423
419
|
|
|
424
|
-
|
|
425
|
-
for line in old_str.split("\n"):
|
|
426
|
-
diff_lines.append(f"-{line}")
|
|
427
|
-
for line in new_str.split("\n"):
|
|
428
|
-
diff_lines.append(f"+{line}")
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
"oldStart": old_start,
|
|
432
|
-
"oldLines": old_str_lines,
|
|
433
|
-
"newStart": old_start,
|
|
434
|
-
"newLines": new_str_lines,
|
|
435
|
-
"lines": diff_lines,
|
|
436
|
-
}
|
|
420
|
+
return {"error": f"Unexpected response: {output}", "stderr": result.stderr}
|
|
@@ -4,10 +4,10 @@ acontext/_utils.py,sha256=GKQH45arKh0sDu64u-5jwrII_ctnU_oChYlgR5lRkfE,1250
|
|
|
4
4
|
acontext/agent/__init__.py,sha256=lx5HqeVDfIxkbGfAt0ju8hkk8EPsgFvKmMrQzjNXR8w,213
|
|
5
5
|
acontext/agent/base.py,sha256=BDTqfqkPrPzvQIH_llOX6S_bkcDw3E30J5x9yd47Q6g,3526
|
|
6
6
|
acontext/agent/disk.py,sha256=q-3DRkLcQLfdAXADJM3GPo6reWPzhKQrnyBiZdBZvIY,22775
|
|
7
|
-
acontext/agent/prompts.py,sha256=
|
|
8
|
-
acontext/agent/sandbox.py,sha256=
|
|
7
|
+
acontext/agent/prompts.py,sha256=awhYYClNAROnTAfzKKD8G5gHfTFs-1KqMUEWCEnlMMQ,4954
|
|
8
|
+
acontext/agent/sandbox.py,sha256=ziGGs2yqJaik_9o7S3f9G-CzcndmRs9fC3WDUi_UGbI,19016
|
|
9
9
|
acontext/agent/skill.py,sha256=uB7teH7OAk4Cy6pOLRCJFBi5KZbyMMgZSLEt77TLB20,12880
|
|
10
|
-
acontext/agent/text_editor.py,sha256=
|
|
10
|
+
acontext/agent/text_editor.py,sha256=mrUtMfAtgpOOwYnh2xQpWRc0lCoE9u2_WMTVRuFiWRQ,13390
|
|
11
11
|
acontext/async_client.py,sha256=WjqW7ruMtlYTBlk6PQ1-1yhjFSBBxGqI3oxrfEqqIpQ,8471
|
|
12
12
|
acontext/client.py,sha256=XggELxXU9-8BcAcHOoiqQZypfPmkhe7SyJ3plHq-A9M,8217
|
|
13
13
|
acontext/client_types.py,sha256=oR6lSDMjEERx0mIVOpKaTXbEu_lfoxe3fDpzA-ALE-8,1063
|
|
@@ -36,6 +36,6 @@ acontext/types/skill.py,sha256=v7OAvtkwTeZAkLG7NQMcwFoSJ4g-HDWjPZ4EoMyU3mk,2779
|
|
|
36
36
|
acontext/types/tool.py,sha256=UlX6JIGRKd96vPayOkyi6pGwFi6w9GwWwAxr4BcqVts,670
|
|
37
37
|
acontext/types/user.py,sha256=dBzHCqULSJ3Sqw7T8nA0U8Sctz76Pd0hm1qsHvtEIBQ,1264
|
|
38
38
|
acontext/uploads.py,sha256=6twnqQOY_eerNuEjeSKsE_3S0IfJUiczXtAy4aXqDl8,1379
|
|
39
|
-
acontext-0.1.
|
|
40
|
-
acontext-0.1.
|
|
41
|
-
acontext-0.1.
|
|
39
|
+
acontext-0.1.6.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
|
|
40
|
+
acontext-0.1.6.dist-info/METADATA,sha256=xUgSw00x4t4EHQR7WLgfwlnjEgXJjD1LH2mk4t_KKSo,888
|
|
41
|
+
acontext-0.1.6.dist-info/RECORD,,
|