acontext 0.1.1__tar.gz → 0.1.3__tar.gz

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 (44) hide show
  1. {acontext-0.1.1 → acontext-0.1.3}/PKG-INFO +2 -2
  2. {acontext-0.1.1 → acontext-0.1.3}/pyproject.toml +1 -1
  3. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/async_client.py +2 -0
  4. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/client.py +2 -0
  5. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/__init__.py +4 -0
  6. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/async_disks.py +92 -0
  7. acontext-0.1.3/src/acontext/resources/async_sandboxes.py +58 -0
  8. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/async_sessions.py +42 -0
  9. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/disks.py +131 -33
  10. acontext-0.1.3/src/acontext/resources/sandboxes.py +58 -0
  11. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/sessions.py +51 -8
  12. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/types/__init__.py +7 -0
  13. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/types/disk.py +6 -3
  14. acontext-0.1.3/src/acontext/types/sandbox.py +20 -0
  15. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/types/session.py +25 -1
  16. {acontext-0.1.1 → acontext-0.1.3}/README.md +0 -0
  17. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/__init__.py +0 -0
  18. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/_constants.py +0 -0
  19. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/_utils.py +0 -0
  20. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/agent/__init__.py +0 -0
  21. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/agent/base.py +0 -0
  22. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/agent/disk.py +0 -0
  23. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/agent/skill.py +0 -0
  24. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/client_types.py +0 -0
  25. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/errors.py +0 -0
  26. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/messages.py +0 -0
  27. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/py.typed +0 -0
  28. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/async_blocks.py +0 -0
  29. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/async_skills.py +0 -0
  30. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/async_spaces.py +0 -0
  31. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/async_tools.py +0 -0
  32. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/async_users.py +0 -0
  33. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/blocks.py +0 -0
  34. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/skills.py +0 -0
  35. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/spaces.py +0 -0
  36. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/tools.py +0 -0
  37. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/resources/users.py +0 -0
  38. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/types/block.py +0 -0
  39. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/types/common.py +0 -0
  40. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/types/skill.py +0 -0
  41. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/types/space.py +0 -0
  42. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/types/tool.py +0 -0
  43. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/types/user.py +0 -0
  44. {acontext-0.1.1 → acontext-0.1.3}/src/acontext/uploads.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: acontext
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Python SDK for the Acontext API
5
5
  Keywords: acontext,sdk,client,api
6
6
  Requires-Dist: httpx>=0.28.1
@@ -10,8 +10,8 @@ Requires-Dist: pydantic>=2.12.3
10
10
  Requires-Dist: urllib3>=2.6.3
11
11
  Requires-Python: >=3.10
12
12
  Project-URL: Homepage, https://github.com/memodb-io/Acontext
13
- Project-URL: Issues, https://github.com/memodb-io/Acontext/issues
14
13
  Project-URL: Repository, https://github.com/memodb-io/Acontext
14
+ Project-URL: Issues, https://github.com/memodb-io/Acontext/issues
15
15
  Description-Content-Type: text/markdown
16
16
 
17
17
  ## acontext client for python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acontext"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "Python SDK for the Acontext API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -14,6 +14,7 @@ from .messages import MessagePart as MessagePart
14
14
  from .uploads import FileUpload as FileUpload
15
15
  from .resources.async_disks import AsyncDisksAPI as AsyncDisksAPI
16
16
  from .resources.async_blocks import AsyncBlocksAPI as AsyncBlocksAPI
17
+ from .resources.async_sandboxes import AsyncSandboxesAPI as AsyncSandboxesAPI
17
18
  from .resources.async_sessions import AsyncSessionsAPI as AsyncSessionsAPI
18
19
  from .resources.async_spaces import AsyncSpacesAPI as AsyncSpacesAPI
19
20
  from .resources.async_tools import AsyncToolsAPI as AsyncToolsAPI
@@ -113,6 +114,7 @@ class AcontextAsyncClient:
113
114
  self.tools = AsyncToolsAPI(self)
114
115
  self.skills = AsyncSkillsAPI(self)
115
116
  self.users = AsyncUsersAPI(self)
117
+ self.sandboxes = AsyncSandboxesAPI(self)
116
118
 
117
119
  @property
118
120
  def base_url(self) -> str:
@@ -14,6 +14,7 @@ from .messages import MessagePart as MessagePart
14
14
  from .uploads import FileUpload as FileUpload
15
15
  from .resources.disks import DisksAPI as DisksAPI
16
16
  from .resources.blocks import BlocksAPI as BlocksAPI
17
+ from .resources.sandboxes import SandboxesAPI as SandboxesAPI
17
18
  from .resources.sessions import SessionsAPI as SessionsAPI
18
19
  from .resources.spaces import SpacesAPI as SpacesAPI
19
20
  from .resources.tools import ToolsAPI as ToolsAPI
@@ -113,6 +114,7 @@ class AcontextClient:
113
114
  self.tools = ToolsAPI(self)
114
115
  self.skills = SkillsAPI(self)
115
116
  self.users = UsersAPI(self)
117
+ self.sandboxes = SandboxesAPI(self)
116
118
 
117
119
  @property
118
120
  def base_url(self) -> str:
@@ -2,6 +2,7 @@
2
2
 
3
3
  from .async_blocks import AsyncBlocksAPI
4
4
  from .async_disks import AsyncDisksAPI, AsyncDiskArtifactsAPI
5
+ from .async_sandboxes import AsyncSandboxesAPI
5
6
  from .async_sessions import AsyncSessionsAPI
6
7
  from .async_spaces import AsyncSpacesAPI
7
8
  from .async_tools import AsyncToolsAPI
@@ -9,6 +10,7 @@ from .async_skills import AsyncSkillsAPI
9
10
  from .async_users import AsyncUsersAPI
10
11
  from .blocks import BlocksAPI
11
12
  from .disks import DisksAPI, DiskArtifactsAPI
13
+ from .sandboxes import SandboxesAPI
12
14
  from .sessions import SessionsAPI
13
15
  from .spaces import SpacesAPI
14
16
  from .tools import ToolsAPI
@@ -19,6 +21,7 @@ __all__ = [
19
21
  "DisksAPI",
20
22
  "DiskArtifactsAPI",
21
23
  "BlocksAPI",
24
+ "SandboxesAPI",
22
25
  "SessionsAPI",
23
26
  "SpacesAPI",
24
27
  "ToolsAPI",
@@ -27,6 +30,7 @@ __all__ = [
27
30
  "AsyncDisksAPI",
28
31
  "AsyncDiskArtifactsAPI",
29
32
  "AsyncBlocksAPI",
33
+ "AsyncSandboxesAPI",
30
34
  "AsyncSessionsAPI",
31
35
  "AsyncSpacesAPI",
32
36
  "AsyncToolsAPI",
@@ -287,3 +287,95 @@ class AsyncDiskArtifactsAPI:
287
287
  "GET", f"/disk/{disk_id}/artifact/glob", params=params
288
288
  )
289
289
  return [Artifact.model_validate(item) for item in data]
290
+
291
+ async def download_to_sandbox(
292
+ self,
293
+ disk_id: str,
294
+ *,
295
+ file_path: str,
296
+ filename: str,
297
+ sandbox_id: str,
298
+ sandbox_path: str,
299
+ ) -> bool:
300
+ """Download an artifact from disk storage to a sandbox environment.
301
+
302
+ Args:
303
+ disk_id: The UUID of the disk containing the artifact.
304
+ file_path: Directory path of the artifact (not including filename).
305
+ filename: The filename of the artifact.
306
+ sandbox_id: The UUID of the target sandbox.
307
+ sandbox_path: Destination directory in the sandbox.
308
+
309
+ Returns:
310
+ True if the download was successful.
311
+
312
+ Example:
313
+ ```python
314
+ success = await client.disks.artifacts.download_to_sandbox(
315
+ disk_id="disk-uuid",
316
+ file_path="/documents/",
317
+ filename="report.pdf",
318
+ sandbox_id="sandbox-uuid",
319
+ sandbox_path="/home/user/"
320
+ )
321
+ print(f"Success: {success}")
322
+ ```
323
+ """
324
+ payload = {
325
+ "file_path": file_path,
326
+ "filename": filename,
327
+ "sandbox_id": sandbox_id,
328
+ "sandbox_path": sandbox_path,
329
+ }
330
+ data = await self._requester.request(
331
+ "POST",
332
+ f"/disk/{disk_id}/artifact/download_to_sandbox",
333
+ json_data=payload,
334
+ )
335
+ return bool(data.get("success", False))
336
+
337
+ async def upload_from_sandbox(
338
+ self,
339
+ disk_id: str,
340
+ *,
341
+ sandbox_id: str,
342
+ sandbox_path: str,
343
+ sandbox_filename: str,
344
+ file_path: str,
345
+ ) -> Artifact:
346
+ """Upload a file from a sandbox environment to disk storage as an artifact.
347
+
348
+ Args:
349
+ disk_id: The UUID of the target disk.
350
+ sandbox_id: The UUID of the source sandbox.
351
+ sandbox_path: Source directory in the sandbox (not including filename).
352
+ sandbox_filename: Filename in the sandbox.
353
+ file_path: Destination directory path on the disk.
354
+
355
+ Returns:
356
+ Artifact containing the created artifact information.
357
+
358
+ Example:
359
+ ```python
360
+ artifact = await client.disks.artifacts.upload_from_sandbox(
361
+ disk_id="disk-uuid",
362
+ sandbox_id="sandbox-uuid",
363
+ sandbox_path="/home/user/",
364
+ sandbox_filename="output.txt",
365
+ file_path="/results/"
366
+ )
367
+ print(f"Created: {artifact.path}{artifact.filename}")
368
+ ```
369
+ """
370
+ payload = {
371
+ "sandbox_id": sandbox_id,
372
+ "sandbox_path": sandbox_path,
373
+ "sandbox_filename": sandbox_filename,
374
+ "file_path": file_path,
375
+ }
376
+ data = await self._requester.request(
377
+ "POST",
378
+ f"/disk/{disk_id}/artifact/upload_from_sandbox",
379
+ json_data=payload,
380
+ )
381
+ return Artifact.model_validate(data)
@@ -0,0 +1,58 @@
1
+ """
2
+ Sandboxes endpoints (async).
3
+ """
4
+
5
+ from ..client_types import AsyncRequesterProtocol
6
+ from ..types.sandbox import (
7
+ SandboxCommandOutput,
8
+ SandboxRuntimeInfo,
9
+ )
10
+ from ..types.tool import FlagResponse
11
+
12
+
13
+ class AsyncSandboxesAPI:
14
+ def __init__(self, requester: AsyncRequesterProtocol) -> None:
15
+ self._requester = requester
16
+
17
+ async def create(self) -> SandboxRuntimeInfo:
18
+ """Create and start a new sandbox.
19
+
20
+ Returns:
21
+ SandboxRuntimeInfo containing the sandbox ID, status, and timestamps.
22
+ """
23
+ data = await self._requester.request("POST", "/sandbox")
24
+ return SandboxRuntimeInfo.model_validate(data)
25
+
26
+ async def exec_command(
27
+ self,
28
+ *,
29
+ sandbox_id: str,
30
+ command: str,
31
+ ) -> SandboxCommandOutput:
32
+ """Execute a shell command in the sandbox.
33
+
34
+ Args:
35
+ sandbox_id: The UUID of the sandbox.
36
+ command: The shell command to execute.
37
+
38
+ Returns:
39
+ SandboxCommandOutput containing stdout, stderr, and exit code.
40
+ """
41
+ data = await self._requester.request(
42
+ "POST",
43
+ f"/sandbox/{sandbox_id}/exec",
44
+ json_data={"command": command},
45
+ )
46
+ return SandboxCommandOutput.model_validate(data)
47
+
48
+ async def kill(self, sandbox_id: str) -> FlagResponse:
49
+ """Kill a running sandbox.
50
+
51
+ Args:
52
+ sandbox_id: The UUID of the sandbox to kill.
53
+
54
+ Returns:
55
+ FlagResponse with status and error message.
56
+ """
57
+ data = await self._requester.request("DELETE", f"/sandbox/{sandbox_id}")
58
+ return FlagResponse.model_validate(data)
@@ -180,6 +180,47 @@ class AsyncSessionsAPI:
180
180
  )
181
181
  return GetTasksOutput.model_validate(data)
182
182
 
183
+ async def get_session_summary(
184
+ self,
185
+ session_id: str,
186
+ *,
187
+ limit: int | None = None,
188
+ ) -> str:
189
+ """Get a summary of all tasks in a session as a formatted string.
190
+
191
+ Args:
192
+ session_id: The UUID of the session.
193
+ limit: Maximum number of tasks to include in the summary. Defaults to None (all tasks).
194
+
195
+ Returns:
196
+ A formatted string containing the session summary with all task information.
197
+ """
198
+ tasks_output = await self.get_tasks(session_id, limit=limit, time_desc=False)
199
+ tasks = tasks_output.items
200
+
201
+ if not tasks:
202
+ return ""
203
+
204
+ parts: list[str] = []
205
+ for task in tasks:
206
+ task_lines = [
207
+ f'<task id="{task.order}" description="{task.data.task_description}">'
208
+ ]
209
+ if task.data.progresses:
210
+ task_lines.append("<progress>")
211
+ for i, p in enumerate(task.data.progresses, 1):
212
+ task_lines.append(f"{i}. {p}")
213
+ task_lines.append("</progress>")
214
+ if task.data.user_preferences:
215
+ task_lines.append("<user_preference>")
216
+ for i, pref in enumerate(task.data.user_preferences, 1):
217
+ task_lines.append(f"{i}. {pref}")
218
+ task_lines.append("</user_preference>")
219
+ task_lines.append("</task>")
220
+ parts.append("\n".join(task_lines))
221
+
222
+ return "\n".join(parts)
223
+
183
224
  async def store_message(
184
225
  self,
185
226
  session_id: str,
@@ -294,6 +335,7 @@ class AsyncSessionsAPI:
294
335
  Each strategy is a dict with 'type' and 'params' keys.
295
336
  Examples:
296
337
  - Remove tool results: [{"type": "remove_tool_result", "params": {"keep_recent_n_tool_results": 3}}]
338
+ - Middle out: [{"type": "middle_out", "params": {"token_reduce_to": 5000}}]
297
339
  - Token limit: [{"type": "token_limit", "params": {"limit_tokens": 20000}}]
298
340
  Defaults to None.
299
341
  pin_editing_strategies_at_message: Message ID to pin editing strategies at.
@@ -37,26 +37,28 @@ class DisksAPI:
37
37
  time_desc: bool | None = None,
38
38
  ) -> ListDisksOutput:
39
39
  """List all disks in the project.
40
-
40
+
41
41
  Args:
42
42
  user: Filter by user identifier. Defaults to None.
43
43
  limit: Maximum number of disks to return. Defaults to None.
44
44
  cursor: Cursor for pagination. Defaults to None.
45
45
  time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
46
-
46
+
47
47
  Returns:
48
48
  ListDisksOutput containing the list of disks and pagination information.
49
49
  """
50
- params = build_params(user=user, limit=limit, cursor=cursor, time_desc=time_desc)
50
+ params = build_params(
51
+ user=user, limit=limit, cursor=cursor, time_desc=time_desc
52
+ )
51
53
  data = self._requester.request("GET", "/disk", params=params or None)
52
54
  return ListDisksOutput.model_validate(data)
53
55
 
54
56
  def create(self, *, user: str | None = None) -> Disk:
55
57
  """Create a new disk.
56
-
58
+
57
59
  Args:
58
60
  user: Optional user identifier string. Defaults to None.
59
-
61
+
60
62
  Returns:
61
63
  The created Disk object.
62
64
  """
@@ -68,7 +70,7 @@ class DisksAPI:
68
70
 
69
71
  def delete(self, disk_id: str) -> None:
70
72
  """Delete a disk by its ID.
71
-
73
+
72
74
  Args:
73
75
  disk_id: The UUID of the disk to delete.
74
76
  """
@@ -83,20 +85,22 @@ class DiskArtifactsAPI:
83
85
  self,
84
86
  disk_id: str,
85
87
  *,
86
- file: FileUpload
87
- | tuple[str, BinaryIO | bytes]
88
- | tuple[str, BinaryIO | bytes, str],
88
+ file: (
89
+ FileUpload
90
+ | tuple[str, BinaryIO | bytes]
91
+ | tuple[str, BinaryIO | bytes, str]
92
+ ),
89
93
  file_path: str | None = None,
90
94
  meta: Mapping[str, Any] | None = None,
91
95
  ) -> Artifact:
92
96
  """Upload a file to create or update an artifact.
93
-
97
+
94
98
  Args:
95
99
  disk_id: The UUID of the disk.
96
100
  file: The file to upload (FileUpload object or tuple format).
97
101
  file_path: Directory path (not including filename), defaults to "/".
98
102
  meta: Custom metadata as JSON-serializable dict, defaults to None.
99
-
103
+
100
104
  Returns:
101
105
  Artifact containing the created/updated artifact information.
102
106
  """
@@ -126,7 +130,7 @@ class DiskArtifactsAPI:
126
130
  expire: int | None = None,
127
131
  ) -> GetArtifactResp:
128
132
  """Get an artifact by disk ID, file path, and filename.
129
-
133
+
130
134
  Args:
131
135
  disk_id: The UUID of the disk.
132
136
  file_path: Directory path (not including filename).
@@ -134,7 +138,7 @@ class DiskArtifactsAPI:
134
138
  with_public_url: Whether to include a presigned public URL. Defaults to None.
135
139
  with_content: Whether to include file content. Defaults to None.
136
140
  expire: URL expiration time in seconds. Defaults to None.
137
-
141
+
138
142
  Returns:
139
143
  GetArtifactResp containing the artifact and optionally public URL and content.
140
144
  """
@@ -145,7 +149,9 @@ class DiskArtifactsAPI:
145
149
  with_content=with_content,
146
150
  expire=expire,
147
151
  )
148
- data = self._requester.request("GET", f"/disk/{disk_id}/artifact", params=params)
152
+ data = self._requester.request(
153
+ "GET", f"/disk/{disk_id}/artifact", params=params
154
+ )
149
155
  return GetArtifactResp.model_validate(data)
150
156
 
151
157
  def update(
@@ -157,13 +163,13 @@ class DiskArtifactsAPI:
157
163
  meta: Mapping[str, Any],
158
164
  ) -> UpdateArtifactResp:
159
165
  """Update an artifact's metadata.
160
-
166
+
161
167
  Args:
162
168
  disk_id: The UUID of the disk.
163
169
  file_path: Directory path (not including filename).
164
170
  filename: The filename of the artifact.
165
171
  meta: Custom metadata as JSON-serializable dict.
166
-
172
+
167
173
  Returns:
168
174
  UpdateArtifactResp containing the updated artifact information.
169
175
  """
@@ -172,7 +178,9 @@ class DiskArtifactsAPI:
172
178
  "file_path": full_path,
173
179
  "meta": json.dumps(cast(Mapping[str, Any], meta)),
174
180
  }
175
- data = self._requester.request("PUT", f"/disk/{disk_id}/artifact", json_data=payload)
181
+ data = self._requester.request(
182
+ "PUT", f"/disk/{disk_id}/artifact", json_data=payload
183
+ )
176
184
  return UpdateArtifactResp.model_validate(data)
177
185
 
178
186
  def delete(
@@ -183,7 +191,7 @@ class DiskArtifactsAPI:
183
191
  filename: str,
184
192
  ) -> None:
185
193
  """Delete an artifact by disk ID, file path, and filename.
186
-
194
+
187
195
  Args:
188
196
  disk_id: The UUID of the disk.
189
197
  file_path: Directory path (not including filename).
@@ -200,18 +208,20 @@ class DiskArtifactsAPI:
200
208
  path: str | None = None,
201
209
  ) -> ListArtifactsResp:
202
210
  """List artifacts in a disk at a specific path.
203
-
211
+
204
212
  Args:
205
213
  disk_id: The UUID of the disk.
206
214
  path: Directory path to list. Defaults to None (root).
207
-
215
+
208
216
  Returns:
209
217
  ListArtifactsResp containing the list of artifacts.
210
218
  """
211
219
  params: dict[str, Any] = {}
212
220
  if path is not None:
213
221
  params["path"] = path
214
- data = self._requester.request("GET", f"/disk/{disk_id}/artifact/ls", params=params or None)
222
+ data = self._requester.request(
223
+ "GET", f"/disk/{disk_id}/artifact/ls", params=params or None
224
+ )
215
225
  return ListArtifactsResp.model_validate(data)
216
226
 
217
227
  def grep_artifacts(
@@ -222,15 +232,15 @@ class DiskArtifactsAPI:
222
232
  limit: int = 100,
223
233
  ) -> list[Artifact]:
224
234
  """Search artifact content using regex pattern.
225
-
235
+
226
236
  Args:
227
237
  disk_id: The disk ID to search in
228
238
  query: Regex pattern to search for in file content
229
239
  limit: Maximum number of results (default 100, max 1000)
230
-
240
+
231
241
  Returns:
232
242
  List of matching artifacts
233
-
243
+
234
244
  Example:
235
245
  ```python
236
246
  # Search for TODO comments in code
@@ -242,9 +252,7 @@ class DiskArtifactsAPI:
242
252
  """
243
253
  params = build_params(query=query, limit=limit)
244
254
  data = self._requester.request(
245
- "GET",
246
- f"/disk/{disk_id}/artifact/grep",
247
- params=params
255
+ "GET", f"/disk/{disk_id}/artifact/grep", params=params
248
256
  )
249
257
  return TypeAdapter(list[Artifact]).validate_python(data)
250
258
 
@@ -256,15 +264,15 @@ class DiskArtifactsAPI:
256
264
  limit: int = 100,
257
265
  ) -> list[Artifact]:
258
266
  """Search artifact paths using glob pattern.
259
-
267
+
260
268
  Args:
261
269
  disk_id: The disk ID to search in
262
270
  query: Glob pattern (e.g., '**/*.py', '*.txt')
263
271
  limit: Maximum number of results (default 100, max 1000)
264
-
272
+
265
273
  Returns:
266
274
  List of matching artifacts
267
-
275
+
268
276
  Example:
269
277
  ```python
270
278
  # Find all Python files
@@ -276,8 +284,98 @@ class DiskArtifactsAPI:
276
284
  """
277
285
  params = build_params(query=query, limit=limit)
278
286
  data = self._requester.request(
279
- "GET",
280
- f"/disk/{disk_id}/artifact/glob",
281
- params=params
287
+ "GET", f"/disk/{disk_id}/artifact/glob", params=params
282
288
  )
283
289
  return TypeAdapter(list[Artifact]).validate_python(data)
290
+
291
+ def download_to_sandbox(
292
+ self,
293
+ disk_id: str,
294
+ *,
295
+ file_path: str,
296
+ filename: str,
297
+ sandbox_id: str,
298
+ sandbox_path: str,
299
+ ) -> bool:
300
+ """Download an artifact from disk storage to a sandbox environment.
301
+
302
+ Args:
303
+ disk_id: The UUID of the disk containing the artifact.
304
+ file_path: Directory path of the artifact (not including filename).
305
+ filename: The filename of the artifact.
306
+ sandbox_id: The UUID of the target sandbox.
307
+ sandbox_path: Destination directory in the sandbox.
308
+
309
+ Returns:
310
+ True if the download was successful.
311
+
312
+ Example:
313
+ ```python
314
+ success = client.disks.artifacts.download_to_sandbox(
315
+ disk_id="disk-uuid",
316
+ file_path="/documents/",
317
+ filename="report.pdf",
318
+ sandbox_id="sandbox-uuid",
319
+ sandbox_path="/home/user/"
320
+ )
321
+ print(f"Success: {success}")
322
+ ```
323
+ """
324
+ payload = {
325
+ "file_path": file_path,
326
+ "filename": filename,
327
+ "sandbox_id": sandbox_id,
328
+ "sandbox_path": sandbox_path,
329
+ }
330
+ data = self._requester.request(
331
+ "POST",
332
+ f"/disk/{disk_id}/artifact/download_to_sandbox",
333
+ json_data=payload,
334
+ )
335
+ return bool(data.get("success", False))
336
+
337
+ def upload_from_sandbox(
338
+ self,
339
+ disk_id: str,
340
+ *,
341
+ sandbox_id: str,
342
+ sandbox_path: str,
343
+ sandbox_filename: str,
344
+ file_path: str,
345
+ ) -> Artifact:
346
+ """Upload a file from a sandbox environment to disk storage as an artifact.
347
+
348
+ Args:
349
+ disk_id: The UUID of the target disk.
350
+ sandbox_id: The UUID of the source sandbox.
351
+ sandbox_path: Source directory in the sandbox (not including filename).
352
+ sandbox_filename: Filename in the sandbox.
353
+ file_path: Destination directory path on the disk.
354
+
355
+ Returns:
356
+ Artifact containing the created artifact information.
357
+
358
+ Example:
359
+ ```python
360
+ artifact = client.disks.artifacts.upload_from_sandbox(
361
+ disk_id="disk-uuid",
362
+ sandbox_id="sandbox-uuid",
363
+ sandbox_path="/home/user/",
364
+ sandbox_filename="output.txt",
365
+ file_path="/results/"
366
+ )
367
+ print(f"Created: {artifact.path}{artifact.filename}")
368
+ ```
369
+ """
370
+ payload = {
371
+ "sandbox_id": sandbox_id,
372
+ "sandbox_path": sandbox_path,
373
+ "sandbox_filename": sandbox_filename,
374
+ "file_path": file_path,
375
+ }
376
+ data = self._requester.request(
377
+ "POST",
378
+ f"/disk/{disk_id}/artifact/upload_from_sandbox",
379
+ json_data=payload,
380
+ )
381
+ return Artifact.model_validate(data)
@@ -0,0 +1,58 @@
1
+ """
2
+ Sandboxes endpoints.
3
+ """
4
+
5
+ from ..client_types import RequesterProtocol
6
+ from ..types.sandbox import (
7
+ SandboxCommandOutput,
8
+ SandboxRuntimeInfo,
9
+ )
10
+ from ..types.tool import FlagResponse
11
+
12
+
13
+ class SandboxesAPI:
14
+ def __init__(self, requester: RequesterProtocol) -> None:
15
+ self._requester = requester
16
+
17
+ def create(self) -> SandboxRuntimeInfo:
18
+ """Create and start a new sandbox.
19
+
20
+ Returns:
21
+ SandboxRuntimeInfo containing the sandbox ID, status, and timestamps.
22
+ """
23
+ data = self._requester.request("POST", "/sandbox")
24
+ return SandboxRuntimeInfo.model_validate(data)
25
+
26
+ def exec_command(
27
+ self,
28
+ *,
29
+ sandbox_id: str,
30
+ command: str,
31
+ ) -> SandboxCommandOutput:
32
+ """Execute a shell command in the sandbox.
33
+
34
+ Args:
35
+ sandbox_id: The UUID of the sandbox.
36
+ command: The shell command to execute.
37
+
38
+ Returns:
39
+ SandboxCommandOutput containing stdout, stderr, and exit code.
40
+ """
41
+ data = self._requester.request(
42
+ "POST",
43
+ f"/sandbox/{sandbox_id}/exec",
44
+ json_data={"command": command},
45
+ )
46
+ return SandboxCommandOutput.model_validate(data)
47
+
48
+ def kill(self, sandbox_id: str) -> FlagResponse:
49
+ """Kill a running sandbox.
50
+
51
+ Args:
52
+ sandbox_id: The UUID of the sandbox to kill.
53
+
54
+ Returns:
55
+ FlagResponse with status and error message.
56
+ """
57
+ data = self._requester.request("DELETE", f"/sandbox/{sandbox_id}")
58
+ return FlagResponse.model_validate(data)
@@ -180,6 +180,47 @@ class SessionsAPI:
180
180
  )
181
181
  return GetTasksOutput.model_validate(data)
182
182
 
183
+ def get_session_summary(
184
+ self,
185
+ session_id: str,
186
+ *,
187
+ limit: int | None = None,
188
+ ) -> str:
189
+ """Get a summary of all tasks in a session as a formatted string.
190
+
191
+ Args:
192
+ session_id: The UUID of the session.
193
+ limit: Maximum number of tasks to include in the summary. Defaults to None (all tasks).
194
+
195
+ Returns:
196
+ A formatted string containing the session summary with all task information.
197
+ """
198
+ tasks_output = self.get_tasks(session_id, limit=limit, time_desc=False)
199
+ tasks = tasks_output.items
200
+
201
+ if not tasks:
202
+ return ""
203
+
204
+ parts: list[str] = []
205
+ for task in tasks:
206
+ task_lines = [
207
+ f'<task id="{task.order}" description="{task.data.task_description}">'
208
+ ]
209
+ if task.data.progresses:
210
+ task_lines.append("<progress>")
211
+ for i, p in enumerate(task.data.progresses, 1):
212
+ task_lines.append(f"{i}. {p}")
213
+ task_lines.append("</progress>")
214
+ if task.data.user_preferences:
215
+ task_lines.append("<user_preference>")
216
+ for i, pref in enumerate(task.data.user_preferences, 1):
217
+ task_lines.append(f"{i}. {pref}")
218
+ task_lines.append("</user_preference>")
219
+ task_lines.append("</task>")
220
+ parts.append("\n".join(task_lines))
221
+
222
+ return "\n".join(parts)
223
+
183
224
  def store_message(
184
225
  self,
185
226
  session_id: str,
@@ -294,6 +335,7 @@ class SessionsAPI:
294
335
  Each strategy is a dict with 'type' and 'params' keys.
295
336
  Examples:
296
337
  - Remove tool results: [{"type": "remove_tool_result", "params": {"keep_recent_n_tool_results": 3}}]
338
+ - Middle out: [{"type": "middle_out", "params": {"token_reduce_to": 5000}}]
297
339
  - Token limit: [{"type": "token_limit", "params": {"limit_tokens": 20000}}]
298
340
  Defaults to None.
299
341
  pin_editing_strategies_at_message: Message ID to pin editing strategies at.
@@ -320,7 +362,9 @@ class SessionsAPI:
320
362
  if edit_strategies is not None:
321
363
  params["edit_strategies"] = json.dumps(edit_strategies)
322
364
  if pin_editing_strategies_at_message is not None:
323
- params["pin_editing_strategies_at_message"] = pin_editing_strategies_at_message
365
+ params["pin_editing_strategies_at_message"] = (
366
+ pin_editing_strategies_at_message
367
+ )
324
368
  data = self._requester.request(
325
369
  "GET", f"/session/{session_id}/messages", params=params or None
326
370
  )
@@ -366,20 +410,19 @@ class SessionsAPI:
366
410
  """
367
411
  data = self._requester.request("GET", f"/session/{session_id}/token_counts")
368
412
  return TokenCounts.model_validate(data)
413
+
369
414
  def messages_observing_status(self, session_id: str) -> MessageObservingStatus:
370
415
  """Get message observing status counts for a session.
371
-
416
+
372
417
  Returns the count of messages by their observing status:
373
418
  observed, in_process, and pending.
374
-
419
+
375
420
  Args:
376
421
  session_id: The UUID of the session.
377
-
422
+
378
423
  Returns:
379
- MessageObservingStatus object containing observed, in_process,
424
+ MessageObservingStatus object containing observed, in_process,
380
425
  pending counts and updated_at timestamp.
381
426
  """
382
- data = self._requester.request(
383
- "GET", f"/session/{session_id}/observing_status"
384
- )
427
+ data = self._requester.request("GET", f"/session/{session_id}/observing_status")
385
428
  return MessageObservingStatus.model_validate(data)
@@ -45,6 +45,10 @@ from .skill import (
45
45
  Skill,
46
46
  SkillCatalogItem,
47
47
  )
48
+ from .sandbox import (
49
+ SandboxCommandOutput,
50
+ SandboxRuntimeInfo,
51
+ )
48
52
  from .user import (
49
53
  GetUserResourcesOutput,
50
54
  ListUsersOutput,
@@ -94,6 +98,9 @@ __all__ = [
94
98
  "SkillCatalogItem",
95
99
  "ListSkillsOutput",
96
100
  "GetSkillFileResp",
101
+ # Sandbox types
102
+ "SandboxCommandOutput",
103
+ "SandboxRuntimeInfo",
97
104
  # User types
98
105
  "GetUserResourcesOutput",
99
106
  "ListUsersOutput",
@@ -43,8 +43,12 @@ class GetArtifactResp(BaseModel):
43
43
  """Response model for getting an artifact."""
44
44
 
45
45
  artifact: Artifact = Field(..., description="Artifact information")
46
- public_url: str | None = Field(None, description="Presigned URL for downloading the artifact")
47
- content: FileContent | None = Field(None, description="Parsed file content if available")
46
+ public_url: str | None = Field(
47
+ None, description="Presigned URL for downloading the artifact"
48
+ )
49
+ content: FileContent | None = Field(
50
+ None, description="Parsed file content if available"
51
+ )
48
52
 
49
53
 
50
54
  class ListArtifactsResp(BaseModel):
@@ -58,4 +62,3 @@ class UpdateArtifactResp(BaseModel):
58
62
  """Response model for updating an artifact."""
59
63
 
60
64
  artifact: Artifact = Field(..., description="Updated artifact information")
61
-
@@ -0,0 +1,20 @@
1
+ """Type definitions for sandbox resources."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class SandboxRuntimeInfo(BaseModel):
7
+ """Runtime information about a sandbox."""
8
+
9
+ sandbox_id: str = Field(..., description="Sandbox ID")
10
+ sandbox_status: str = Field(..., description="Sandbox status (running, killed, paused, error)")
11
+ sandbox_created_at: str = Field(..., description="ISO 8601 formatted creation timestamp")
12
+ sandbox_expires_at: str = Field(..., description="ISO 8601 formatted expiration timestamp")
13
+
14
+
15
+ class SandboxCommandOutput(BaseModel):
16
+ """Output from executing a command in a sandbox."""
17
+
18
+ stdout: str = Field(..., description="Standard output from the command")
19
+ stderr: str = Field(..., description="Standard error from the command")
20
+ exit_code: int = Field(..., description="Exit code of the command")
@@ -89,10 +89,34 @@ class TokenLimitStrategy(TypedDict):
89
89
  params: TokenLimitParams
90
90
 
91
91
 
92
+ class MiddleOutParams(TypedDict):
93
+ """Parameters for the middle_out edit strategy.
94
+
95
+ Attributes:
96
+ token_reduce_to: Target token count to reduce the prompt to. Required parameter.
97
+ """
98
+
99
+ token_reduce_to: int
100
+
101
+
102
+ class MiddleOutStrategy(TypedDict):
103
+ """Edit strategy to reduce prompt size by removing middle messages.
104
+
105
+ Example:
106
+ {"type": "middle_out", "params": {"token_reduce_to": 5000}}
107
+ """
108
+
109
+ type: Literal["middle_out"]
110
+ params: MiddleOutParams
111
+
112
+
92
113
  # Union type for all edit strategies
93
114
  # When adding new strategies, add them to this Union: EditStrategy = Union[RemoveToolResultStrategy, OtherStrategy, ...]
94
115
  EditStrategy = Union[
95
- RemoveToolResultStrategy, RemoveToolCallParamsStrategy, TokenLimitStrategy
116
+ RemoveToolResultStrategy,
117
+ RemoveToolCallParamsStrategy,
118
+ TokenLimitStrategy,
119
+ MiddleOutStrategy,
96
120
  ]
97
121
 
98
122
 
File without changes
File without changes