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 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
- - Do not try to install new packages and libraries with pip as there is no internet access
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": "The operation to perform: 'view', 'create', or 'str_replace'",
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": "For 'create' command: the content to write to the file",
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": "For 'str_replace' command: the exact string to find and replace",
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": "For 'str_replace' command: the string to replace old_str with",
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
- "description": "For 'view' command: optional [start_line, end_line] to view specific lines",
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
 
@@ -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
- # First check if file exists and get total lines
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
- if "FILE_NOT_FOUND" in result.stdout or result.exit_code != 0:
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
- cmd = f"sed -n '{start_line},{end_line}p' {escape_for_shell(path)} | nl -ba -v {start_line}"
50
+ view_cmd = (
51
+ f"sed -n '{start_line},{end_line}p' {escaped_path} | nl -ba -v {start_line}"
52
+ )
55
53
  else:
56
- cmd = f"nl -ba {escape_for_shell(path)}"
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"Failed to view file: {path}",
72
+ "error": f"File not found: {path}",
68
73
  "stderr": result.stderr,
69
74
  }
70
75
 
71
- # Count lines in output
72
- content_lines = (
73
- result.stdout.rstrip("\n").split("\n") if result.stdout.strip() else []
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": result.stdout,
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
- # Check if file already exists
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
- write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
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=write_cmd,
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 diff information or error details.
163
+ A dict with success message or error details.
155
164
  """
156
- # First read the file content
157
- read_cmd = f"cat {escape_for_shell(path)}"
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=read_cmd,
196
+ command=cmd,
161
197
  timeout=timeout,
162
198
  )
163
199
 
164
- if result.exit_code != 0:
165
- return {
166
- "error": f"File not found: {path}",
167
- "stderr": result.stderr,
168
- }
200
+ output = result.stdout.strip()
169
201
 
170
- original_content = result.stdout
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
- # Check if old_str exists in the file
173
- if old_str not in original_content:
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
- # Count occurrences
179
- occurrences = original_content.count(old_str)
180
- if occurrences > 1:
208
+ if output.startswith("MULTIPLE:"):
209
+ count = output.split(":")[1]
181
210
  return {
182
- "error": f"Multiple occurrences ({occurrences}) of the string found. Please provide more context to make the match unique.",
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
- # Perform the replacement
186
- new_content = original_content.replace(old_str, new_str, 1)
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
- # Write the new content
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", path: str, view_range: list | None, timeout: float | None
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
- check_cmd = f"wc -l < {escape_for_shell(path)} 2>/dev/null || echo 'FILE_NOT_FOUND'"
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
- cmd = f"sed -n '{start_line},{end_line}p' {escape_for_shell(path)} | nl -ba -v {start_line}"
248
+ view_cmd = (
249
+ f"sed -n '{start_line},{end_line}p' {escaped_path} | nl -ba -v {start_line}"
250
+ )
272
251
  else:
273
- cmd = f"nl -ba {escape_for_shell(path)}"
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"Failed to view file: {path}",
270
+ "error": f"File not found: {path}",
285
271
  "stderr": result.stderr,
286
272
  }
287
273
 
288
- content_lines = (
289
- result.stdout.rstrip("\n").split("\n") if result.stdout.strip() else []
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": result.stdout,
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
- check_cmd = f"test -f {escape_for_shell(path)} && echo 'EXISTS' || echo 'NEW'"
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
- write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
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=write_cmd,
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", path: str, old_str: str, new_str: str, timeout: float | None
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 diff information or error details.
365
+ A dict with success message or error details.
368
366
  """
369
- read_cmd = f"cat {escape_for_shell(path)}"
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=read_cmd,
398
+ command=cmd,
373
399
  timeout=timeout,
374
400
  )
375
401
 
376
- if result.exit_code != 0:
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
- old_start = 1
401
- for i, (old_line, new_line) in enumerate(zip(old_lines, new_lines)):
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
- encoded_content = base64.b64encode(new_content.encode()).decode()
407
- write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
407
+ if output == "NOT_FOUND":
408
+ return {"error": f"String not found in file: {old_str[:50]}..."}
408
409
 
409
- result = await ctx.client.sandboxes.exec_command(
410
- sandbox_id=ctx.sandbox_id,
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"Failed to write file: {path}",
418
- "stderr": result.stderr,
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
- old_str_lines = old_str.count("\n") + 1
422
- new_str_lines = new_str.count("\n") + 1
417
+ if output == "SUCCESS":
418
+ return {"msg": "Successfully replaced text at exactly one location."}
423
419
 
424
- diff_lines = []
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}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: acontext
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Python SDK for the Acontext API
5
5
  Keywords: acontext,sdk,client,api
6
6
  Requires-Dist: httpx>=0.28.1
@@ -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=Y20C78f9PacjpyN_I_urQBDzoiAeVSXE4Cj5BV67Qhg,4865
8
- acontext/agent/sandbox.py,sha256=eGN_yBTo3jWNhxTZMi-Rkr0ALJ6YP5WZbF8u0oBPn2Y,18209
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=HsmDexX2wqacaeIwkCyDrojZDQiI9mr4krt6phSKS7g,13247
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.4.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
40
- acontext-0.1.4.dist-info/METADATA,sha256=wvxyJ9Rq48wlqsbGdig2RELL5RvXYGtVENosB7a0nGU,888
41
- acontext-0.1.4.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.26
2
+ Generator: uv 0.9.27
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any