langchain-agentcore-codeinterpreter 0.0.1__tar.gz → 0.0.2__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.
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/PKG-INFO +1 -1
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/langchain_agentcore_codeinterpreter/sandbox.py +237 -5
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/pyproject.toml +1 -1
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/tests/integration_tests/test_integration.py +44 -4
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/tests/unit_tests/test_imports.py +7 -0
- langchain_agentcore_codeinterpreter-0.0.2/tests/unit_tests/test_sandbox.py +473 -0
- langchain_agentcore_codeinterpreter-0.0.2/uv.lock +2470 -0
- langchain_agentcore_codeinterpreter-0.0.1/tests/unit_tests/test_sandbox.py +0 -206
- langchain_agentcore_codeinterpreter-0.0.1/uv.lock +0 -2378
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/.gitignore +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/LICENSE +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/Makefile +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/README.md +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/langchain_agentcore_codeinterpreter/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/scripts/check_imports.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/tests/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/tests/integration_tests/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/tests/integration_tests/test_compile.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/tests/unit_tests/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/tests/unit_tests/test_stream_parsing.py +0 -0
{langchain_agentcore_codeinterpreter-0.0.1 → langchain_agentcore_codeinterpreter-0.0.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-agentcore-codeinterpreter
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Amazon Bedrock AgentCore Code Interpreter sandbox integration for Deep Agents
|
|
5
5
|
Project-URL: Source Code, https://github.com/langchain-ai/langchain-aws/tree/main/libs/agentcore-codeinterpreter
|
|
6
6
|
Project-URL: Repository, https://github.com/langchain-ai/langchain-aws
|
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import base64
|
|
6
7
|
import logging
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
9
|
from typing import TYPE_CHECKING, Any
|
|
8
10
|
|
|
11
|
+
from botocore.exceptions import ClientError
|
|
9
12
|
from deepagents.backends.protocol import (
|
|
13
|
+
EditResult,
|
|
10
14
|
ExecuteResponse,
|
|
11
15
|
FileDownloadResponse,
|
|
12
16
|
FileUploadResponse,
|
|
17
|
+
ReadResult,
|
|
18
|
+
WriteResult,
|
|
13
19
|
)
|
|
14
20
|
from deepagents.backends.sandbox import BaseSandbox
|
|
15
21
|
|
|
@@ -18,6 +24,25 @@ if TYPE_CHECKING:
|
|
|
18
24
|
|
|
19
25
|
logger = logging.getLogger(__name__)
|
|
20
26
|
|
|
27
|
+
# Dedicated thread pool for AgentCore boto3 calls. Isolates sandbox I/O
|
|
28
|
+
# from the default asyncio executor so long-running stream reads don't
|
|
29
|
+
# starve other async work (LLM calls, tool dispatch, etc.).
|
|
30
|
+
_AGENTCORE_EXECUTOR = ThreadPoolExecutor(
|
|
31
|
+
max_workers=4, thread_name_prefix="agentcore-sandbox"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SessionExpiredError(Exception):
|
|
36
|
+
"""Raised when the AgentCore session has expired or been terminated."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, session_id: str, original: ClientError) -> None:
|
|
39
|
+
self.session_id = session_id
|
|
40
|
+
self.original = original
|
|
41
|
+
super().__init__(
|
|
42
|
+
f"AgentCore session '{session_id}' has expired or was terminated. "
|
|
43
|
+
f"Start a new session to continue."
|
|
44
|
+
)
|
|
45
|
+
|
|
21
46
|
|
|
22
47
|
def _extract_text_from_stream(response: dict[str, Any]) -> tuple[str, int | None]:
|
|
23
48
|
"""Extract text output and exit code from a code interpreter response stream.
|
|
@@ -118,6 +143,10 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
118
143
|
``download_files()``, and ``upload_files()`` methods using AgentCore's
|
|
119
144
|
streaming API.
|
|
120
145
|
|
|
146
|
+
Async methods (``aexecute``, ``awrite``, etc.) use a dedicated thread
|
|
147
|
+
pool executor to avoid blocking the default ``asyncio`` executor with
|
|
148
|
+
long-running boto3 stream reads.
|
|
149
|
+
|
|
121
150
|
The caller is responsible for managing the interpreter lifecycle
|
|
122
151
|
(``start()`` / ``stop()``).
|
|
123
152
|
|
|
@@ -159,11 +188,48 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
159
188
|
"""
|
|
160
189
|
return path.lstrip("/")
|
|
161
190
|
|
|
191
|
+
def _invoke(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
192
|
+
"""Invoke the interpreter and eagerly consume the response stream.
|
|
193
|
+
|
|
194
|
+
AgentCore's ``invoke_code_interpreter`` returns a lazy EventStream
|
|
195
|
+
that holds the HTTP connection open until fully iterated. Consuming
|
|
196
|
+
it eagerly releases the connection promptly, which prevents thread
|
|
197
|
+
starvation when multiple sandbox calls are in-flight under
|
|
198
|
+
``asyncio.to_thread`` or a thread pool executor.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
method: The interpreter method name (e.g. ``executeCommand``).
|
|
202
|
+
params: Parameters to pass to the method.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Response dict with the ``"stream"`` key materialized as a list.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
SessionExpiredError: If the session has expired or been terminated.
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
response = self._interpreter.invoke(method=method, params=params)
|
|
212
|
+
except ClientError as exc:
|
|
213
|
+
error_code = exc.response.get("Error", {}).get("Code", "")
|
|
214
|
+
if error_code == "ResourceNotFoundException":
|
|
215
|
+
raise SessionExpiredError(self.id, exc) from exc
|
|
216
|
+
raise
|
|
217
|
+
|
|
218
|
+
# Eagerly consume the lazy EventStream to release the HTTP connection.
|
|
219
|
+
if "stream" in response:
|
|
220
|
+
response["stream"] = list(response["stream"])
|
|
221
|
+
|
|
222
|
+
return response
|
|
223
|
+
|
|
162
224
|
@property
|
|
163
225
|
def id(self) -> str:
|
|
164
226
|
"""Return the AgentCore session ID."""
|
|
165
227
|
return self._interpreter.session_id or ""
|
|
166
228
|
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
# Sync methods
|
|
231
|
+
# ------------------------------------------------------------------
|
|
232
|
+
|
|
167
233
|
def execute(
|
|
168
234
|
self,
|
|
169
235
|
command: str,
|
|
@@ -183,7 +249,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
183
249
|
flag.
|
|
184
250
|
"""
|
|
185
251
|
try:
|
|
186
|
-
response = self.
|
|
252
|
+
response = self._invoke(
|
|
187
253
|
method="executeCommand", params={"command": command}
|
|
188
254
|
)
|
|
189
255
|
output, exit_code = _extract_text_from_stream(response)
|
|
@@ -192,6 +258,19 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
192
258
|
exit_code=exit_code if exit_code is not None else 0,
|
|
193
259
|
truncated=False,
|
|
194
260
|
)
|
|
261
|
+
except SessionExpiredError:
|
|
262
|
+
logger.error(
|
|
263
|
+
"AgentCore session expired while executing command: %s",
|
|
264
|
+
command[:80],
|
|
265
|
+
)
|
|
266
|
+
return ExecuteResponse(
|
|
267
|
+
output=(
|
|
268
|
+
"Error: AgentCore session has expired. "
|
|
269
|
+
"Start a new session to continue."
|
|
270
|
+
),
|
|
271
|
+
exit_code=1,
|
|
272
|
+
truncated=False,
|
|
273
|
+
)
|
|
195
274
|
except Exception as exc:
|
|
196
275
|
logger.exception("Error executing command: %s", command[:80])
|
|
197
276
|
msg = f"Error executing command: {exc}"
|
|
@@ -216,7 +295,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
216
295
|
"""
|
|
217
296
|
try:
|
|
218
297
|
relative_paths = [self._to_relative_path(p) for p in paths]
|
|
219
|
-
response = self.
|
|
298
|
+
response = self._invoke(
|
|
220
299
|
method="readFiles", params={"paths": relative_paths}
|
|
221
300
|
)
|
|
222
301
|
file_contents = _extract_files_from_stream(response, paths)
|
|
@@ -229,6 +308,12 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
229
308
|
)
|
|
230
309
|
for path in paths
|
|
231
310
|
]
|
|
311
|
+
except SessionExpiredError:
|
|
312
|
+
logger.error("AgentCore session expired while downloading files: %s", paths)
|
|
313
|
+
return [
|
|
314
|
+
FileDownloadResponse(path=path, content=None, error="permission_denied")
|
|
315
|
+
for path in paths
|
|
316
|
+
]
|
|
232
317
|
except Exception:
|
|
233
318
|
logger.exception("Error downloading files: %s", paths)
|
|
234
319
|
return [
|
|
@@ -261,13 +346,160 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
261
346
|
|
|
262
347
|
try:
|
|
263
348
|
if file_list:
|
|
264
|
-
self.
|
|
265
|
-
method="writeFiles", params={"content": file_list}
|
|
266
|
-
)
|
|
349
|
+
self._invoke(method="writeFiles", params={"content": file_list})
|
|
267
350
|
return [FileUploadResponse(path=path, error=None) for path, _ in files]
|
|
351
|
+
except SessionExpiredError:
|
|
352
|
+
logger.error(
|
|
353
|
+
"AgentCore session expired while uploading files: %s",
|
|
354
|
+
[p for p, _ in files],
|
|
355
|
+
)
|
|
356
|
+
return [
|
|
357
|
+
FileUploadResponse(path=path, error="permission_denied")
|
|
358
|
+
for path, _ in files
|
|
359
|
+
]
|
|
268
360
|
except Exception:
|
|
269
361
|
logger.exception("Error uploading files: %s", [p for p, _ in files])
|
|
270
362
|
return [
|
|
271
363
|
FileUploadResponse(path=path, error="permission_denied")
|
|
272
364
|
for path, _ in files
|
|
273
365
|
]
|
|
366
|
+
|
|
367
|
+
# ------------------------------------------------------------------
|
|
368
|
+
# Async overrides — use a dedicated executor to avoid starving the
|
|
369
|
+
# default asyncio thread pool with long-running boto3 stream reads.
|
|
370
|
+
# ------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
async def aexecute(
|
|
373
|
+
self,
|
|
374
|
+
command: str,
|
|
375
|
+
*,
|
|
376
|
+
timeout: int | None = None,
|
|
377
|
+
) -> ExecuteResponse:
|
|
378
|
+
"""Async version of :meth:`execute`.
|
|
379
|
+
|
|
380
|
+
Runs the sync method in a dedicated thread pool executor to avoid
|
|
381
|
+
blocking the default ``asyncio`` executor.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
command: Shell command string to execute.
|
|
385
|
+
timeout: Unused. Accepted for interface compatibility.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Response containing the command output, exit code, and truncation
|
|
389
|
+
flag.
|
|
390
|
+
"""
|
|
391
|
+
loop = asyncio.get_running_loop()
|
|
392
|
+
return await loop.run_in_executor(
|
|
393
|
+
_AGENTCORE_EXECUTOR,
|
|
394
|
+
lambda: self.execute(command, timeout=timeout),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
async def aread(
|
|
398
|
+
self,
|
|
399
|
+
file_path: str,
|
|
400
|
+
offset: int = 0,
|
|
401
|
+
limit: int = 2000,
|
|
402
|
+
) -> ReadResult:
|
|
403
|
+
"""Async version of :meth:`read`.
|
|
404
|
+
|
|
405
|
+
Runs the sync method in a dedicated thread pool executor.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
file_path: Absolute path to the file to read.
|
|
409
|
+
offset: Starting line number (0-indexed).
|
|
410
|
+
limit: Maximum number of lines to return.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
``ReadResult`` with ``file_data`` on success or ``error`` on
|
|
414
|
+
failure.
|
|
415
|
+
"""
|
|
416
|
+
loop = asyncio.get_running_loop()
|
|
417
|
+
return await loop.run_in_executor(
|
|
418
|
+
_AGENTCORE_EXECUTOR,
|
|
419
|
+
lambda: self.read(file_path, offset, limit),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
async def awrite(
|
|
423
|
+
self,
|
|
424
|
+
file_path: str,
|
|
425
|
+
content: str,
|
|
426
|
+
) -> WriteResult:
|
|
427
|
+
"""Async version of :meth:`write`.
|
|
428
|
+
|
|
429
|
+
Runs the sync method in a dedicated thread pool executor.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
file_path: Absolute path for the new file.
|
|
433
|
+
content: UTF-8 text content to write.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
``WriteResult`` with ``path`` on success or ``error`` on failure.
|
|
437
|
+
"""
|
|
438
|
+
loop = asyncio.get_running_loop()
|
|
439
|
+
return await loop.run_in_executor(
|
|
440
|
+
_AGENTCORE_EXECUTOR,
|
|
441
|
+
lambda: self.write(file_path, content),
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
async def aedit(
|
|
445
|
+
self,
|
|
446
|
+
file_path: str,
|
|
447
|
+
old_string: str,
|
|
448
|
+
new_string: str,
|
|
449
|
+
replace_all: bool = False, # noqa: FBT001, FBT002
|
|
450
|
+
) -> EditResult:
|
|
451
|
+
"""Async version of :meth:`edit`.
|
|
452
|
+
|
|
453
|
+
Runs the sync method in a dedicated thread pool executor.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
file_path: Absolute path to the file to edit.
|
|
457
|
+
old_string: The exact substring to find.
|
|
458
|
+
new_string: The replacement string.
|
|
459
|
+
replace_all: If ``True``, replace every occurrence.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
``EditResult`` with ``path`` and ``occurrences`` on success,
|
|
463
|
+
or ``error`` on failure.
|
|
464
|
+
"""
|
|
465
|
+
loop = asyncio.get_running_loop()
|
|
466
|
+
return await loop.run_in_executor(
|
|
467
|
+
_AGENTCORE_EXECUTOR,
|
|
468
|
+
lambda: self.edit(file_path, old_string, new_string, replace_all),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
async def aupload_files(
|
|
472
|
+
self, files: list[tuple[str, bytes]]
|
|
473
|
+
) -> list[FileUploadResponse]:
|
|
474
|
+
"""Async version of :meth:`upload_files`.
|
|
475
|
+
|
|
476
|
+
Runs the sync method in a dedicated thread pool executor.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
files: List of ``(path, content)`` tuples to upload.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
List of :class:`FileUploadResponse` objects.
|
|
483
|
+
"""
|
|
484
|
+
loop = asyncio.get_running_loop()
|
|
485
|
+
return await loop.run_in_executor(
|
|
486
|
+
_AGENTCORE_EXECUTOR,
|
|
487
|
+
lambda: self.upload_files(files),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
491
|
+
"""Async version of :meth:`download_files`.
|
|
492
|
+
|
|
493
|
+
Runs the sync method in a dedicated thread pool executor.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
paths: List of file paths to download.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
List of :class:`FileDownloadResponse` objects.
|
|
500
|
+
"""
|
|
501
|
+
loop = asyncio.get_running_loop()
|
|
502
|
+
return await loop.run_in_executor(
|
|
503
|
+
_AGENTCORE_EXECUTOR,
|
|
504
|
+
lambda: self.download_files(paths),
|
|
505
|
+
)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langchain-agentcore-codeinterpreter"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.2"
|
|
8
8
|
description = "Amazon Bedrock AgentCore Code Interpreter sandbox integration for Deep Agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -6,8 +6,9 @@ Code Interpreter service is available.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import asyncio
|
|
9
10
|
import os
|
|
10
|
-
from typing import TYPE_CHECKING
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
11
12
|
|
|
12
13
|
import pytest
|
|
13
14
|
|
|
@@ -16,7 +17,6 @@ from langchain_agentcore_codeinterpreter import AgentCoreSandbox
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from collections.abc import Iterator
|
|
18
19
|
|
|
19
|
-
|
|
20
20
|
_SKIP_REASON = "AWS credentials not configured (set AWS_REGION or AWS_DEFAULT_REGION)"
|
|
21
21
|
|
|
22
22
|
|
|
@@ -83,15 +83,55 @@ class TestAgentCoreSandboxIntegration:
|
|
|
83
83
|
|
|
84
84
|
def test_binary_roundtrip(self, sandbox: AgentCoreSandbox) -> None:
|
|
85
85
|
"""Binary content uploaded via base64 may be returned as base64 text."""
|
|
86
|
+
import base64
|
|
87
|
+
|
|
86
88
|
content = b"\x00\x01\x02\xff\xfe\xfd"
|
|
87
89
|
sandbox.upload_files([("binary_test.bin", content)])
|
|
88
90
|
results = sandbox.download_files(["binary_test.bin"])
|
|
89
91
|
assert results[0].error is None
|
|
90
92
|
# AgentCore returns blob uploads as text containing the base64 string
|
|
91
|
-
import base64
|
|
92
|
-
|
|
93
93
|
assert results[0].content in (content, base64.b64encode(content))
|
|
94
94
|
|
|
95
95
|
def test_session_id(self, sandbox: AgentCoreSandbox) -> None:
|
|
96
96
|
"""The sandbox should have a non-empty session ID."""
|
|
97
97
|
assert sandbox.id != ""
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# Async integration tests
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def test_aexecute_echo(self, sandbox: AgentCoreSandbox) -> None:
|
|
104
|
+
"""aexecute() should work against a live session."""
|
|
105
|
+
result = asyncio.run(sandbox.aexecute("echo async hello"))
|
|
106
|
+
assert result.exit_code == 0
|
|
107
|
+
assert "async hello" in result.output
|
|
108
|
+
|
|
109
|
+
def test_aexecute_python(self, sandbox: AgentCoreSandbox) -> None:
|
|
110
|
+
"""aexecute() should handle Python execution."""
|
|
111
|
+
result = asyncio.run(sandbox.aexecute("python3 -c \"print('async works')\""))
|
|
112
|
+
assert result.exit_code == 0
|
|
113
|
+
assert "async works" in result.output
|
|
114
|
+
|
|
115
|
+
def test_aupload_and_adownload_roundtrip(self, sandbox: AgentCoreSandbox) -> None:
|
|
116
|
+
"""Async upload and download should produce identical content."""
|
|
117
|
+
content = b"async round trip content"
|
|
118
|
+
upload_result = asyncio.run(
|
|
119
|
+
sandbox.aupload_files([("async_roundtrip.txt", content)])
|
|
120
|
+
)
|
|
121
|
+
assert upload_result[0].error is None
|
|
122
|
+
|
|
123
|
+
download_result = asyncio.run(sandbox.adownload_files(["async_roundtrip.txt"]))
|
|
124
|
+
assert download_result[0].error is None
|
|
125
|
+
assert download_result[0].content == content
|
|
126
|
+
|
|
127
|
+
def test_concurrent_aexecute(self, sandbox: AgentCoreSandbox) -> None:
|
|
128
|
+
"""Multiple concurrent async executions should not deadlock."""
|
|
129
|
+
|
|
130
|
+
async def run_concurrent() -> list[Any]:
|
|
131
|
+
tasks = [sandbox.aexecute(f"echo concurrent-{i}") for i in range(3)]
|
|
132
|
+
return await asyncio.gather(*tasks)
|
|
133
|
+
|
|
134
|
+
results = asyncio.run(run_concurrent())
|
|
135
|
+
for i, result in enumerate(results):
|
|
136
|
+
assert result.exit_code == 0
|
|
137
|
+
assert f"concurrent-{i}" in result.output
|
|
@@ -15,3 +15,10 @@ def test_import_package() -> None:
|
|
|
15
15
|
def test_import_sandbox_class() -> None:
|
|
16
16
|
"""The public AgentCoreSandbox class should be importable."""
|
|
17
17
|
assert AgentCoreSandbox is not None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_import_session_expired_error() -> None:
|
|
21
|
+
"""SessionExpiredError should be importable from the sandbox module."""
|
|
22
|
+
from langchain_agentcore_codeinterpreter.sandbox import SessionExpiredError
|
|
23
|
+
|
|
24
|
+
assert SessionExpiredError is not None
|