Skip to content

Commit ebbba0d

Browse files
committed
updated llm guidance based on observations about how it seemed to decide between exec and execAsync
1 parent 5b87a49 commit ebbba0d

4 files changed

Lines changed: 92 additions & 13 deletions

File tree

README-SDK.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,13 @@ print(f"Devbox {info.name} is {info.status}")
160160
Execute commands synchronously or asynchronously:
161161

162162
```python
163-
# Synchronous command execution (waits for completion)
163+
# exec blocks until completion - use for commands that return immediately
164164
result = devbox.cmd.exec("ls -la")
165165
print("Output:", result.stdout())
166166
print("Exit code:", result.exit_code)
167167
print("Success:", result.success)
168168

169-
# Asynchronous command execution (returns immediately)
169+
# exec_async returns immediately - use for long-running processes
170170
execution = devbox.cmd.exec_async("npm run dev")
171171

172172
# Check execution status
@@ -393,11 +393,30 @@ async with await runloop.devbox.create(name="temp-devbox") as devbox:
393393
# devbox is automatically shutdown when exiting the context
394394
```
395395

396+
#### Devbox Logs
397+
398+
Retrieve logs from a devbox, optionally filtered by execution ID or shell name:
399+
400+
```python
401+
# Get all devbox logs
402+
logs = devbox.logs()
403+
for log in logs.logs:
404+
print(f"[{log.level}] {log.message}")
405+
406+
# Filter logs by execution ID
407+
result = devbox.cmd.exec('echo "hello"')
408+
exec_logs = devbox.logs(execution_id=result.execution_id)
409+
410+
# Filter logs by shell name
411+
shell_logs = devbox.logs(shell_name="my-shell")
412+
```
413+
396414
**Key methods:**
397415

398416
- `devbox.get_info()` - Get devbox details and status
399417
- `devbox.cmd.exec()` - Execute commands synchronously
400418
- `devbox.cmd.exec_async()` - Execute commands asynchronously
419+
- `devbox.logs()` - Retrieve devbox logs (optionally filter by execution_id or shell_name)
401420
- `devbox.file.read()` - Read file contents
402421
- `devbox.file.write()` - Write file contents
403422
- `devbox.file.upload()` - Upload files

llms.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
- **Prefer `AsyncRunloopSDK` over `RunloopSDK`** for better concurrency and performance; all SDK methods have async equivalents
2424
- Use `async with await runloop.devbox.create()` for automatic cleanup via context manager
2525
- For resources without SDK coverage (e.g., secrets, benchmarks), use `runloop.api.*` as a fallback
26-
- Use `await devbox.cmd.exec('command')` for most commands—blocks until completion, returns `ExecutionResult` with stdout/stderr
27-
- Use `await devbox.cmd.exec_async('command')` for long-running or background processes (servers, watchers)—returns immediately with `Execution` handle to check status, get result, or kill
26+
- Use `await devbox.cmd.exec('command')` for commands expected to return immediately (e.g., `echo`, `pwd`, `cat`)—blocks until completion, returns `ExecutionResult` with stdout/stderr
27+
- Use `await devbox.cmd.exec_async('command')` for long-running or background processes (servers, watchers, builds)—returns immediately with `Execution` handle to check status, get result, or kill
2828
- Both `exec` and `exec_async` support streaming callbacks (`stdout`, `stderr`, `output`) for real-time output
2929
- Call `await devbox.shutdown()` to clean up resources that are no longer in use.
3030
- Streaming callbacks (`stdout`, `stderr`, `output`) must be synchronous functions even with async SDK

tests/smoketests/sdk/test_async_devbox.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,3 +1064,55 @@ async def test_shell_exec_async_with_both_streams(self, devbox: AsyncDevbox) ->
10641064
# Verify streaming captured same data as result
10651065
assert stdout_combined == await result.stdout()
10661066
assert stderr_combined == await result.stderr()
1067+
1068+
1069+
class TestAsyncDevboxLogs:
1070+
"""Test async devbox logs retrieval functionality."""
1071+
1072+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
1073+
async def test_logs_basic(self, shared_devbox: AsyncDevbox) -> None:
1074+
"""Test retrieving devbox logs returns valid response structure."""
1075+
test_message = "async basic log test message"
1076+
result = await shared_devbox.cmd.exec(f'echo "{test_message}"')
1077+
assert result.exit_code == 0
1078+
1079+
logs = await shared_devbox.logs()
1080+
1081+
assert logs is not None
1082+
assert hasattr(logs, "logs")
1083+
assert isinstance(logs.logs, list)
1084+
log_content = " ".join(str(log) for log in logs.logs)
1085+
assert test_message in log_content
1086+
1087+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
1088+
async def test_logs_with_execution_filter(self, shared_devbox: AsyncDevbox) -> None:
1089+
"""Test retrieving devbox logs filtered by execution ID."""
1090+
test_message = "async filtered log test"
1091+
result = await shared_devbox.cmd.exec(f'echo "{test_message}"')
1092+
assert result.exit_code == 0
1093+
1094+
logs = await shared_devbox.logs(execution_id=result.execution_id)
1095+
1096+
assert logs is not None
1097+
assert hasattr(logs, "logs")
1098+
assert isinstance(logs.logs, list)
1099+
log_content = " ".join(str(log) for log in logs.logs)
1100+
assert test_message in log_content
1101+
1102+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
1103+
async def test_logs_with_shell_name_filter(self, shared_devbox: AsyncDevbox) -> None:
1104+
"""Test retrieving devbox logs filtered by shell name."""
1105+
shell_name = "async-test-logs-shell"
1106+
shell = shared_devbox.shell(shell_name)
1107+
1108+
test_message = "async shell log test"
1109+
result = await shell.exec(f'echo "{test_message}"')
1110+
assert result.exit_code == 0
1111+
1112+
logs = await shared_devbox.logs(shell_name=shell_name)
1113+
1114+
assert logs is not None
1115+
assert hasattr(logs, "logs")
1116+
assert isinstance(logs.logs, list)
1117+
log_content = " ".join(str(log) for log in logs.logs)
1118+
assert test_message in log_content

tests/smoketests/sdk/test_devbox.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,39 +1058,47 @@ class TestDevboxLogs:
10581058
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
10591059
def test_logs_basic(self, shared_devbox: Devbox) -> None:
10601060
"""Test retrieving devbox logs returns valid response structure."""
1061-
# Fetch logs - the API may return empty logs depending on timing
1061+
test_message = "basic log test message"
1062+
result = shared_devbox.cmd.exec(f'echo "{test_message}"')
1063+
assert result.exit_code == 0
1064+
10621065
logs = shared_devbox.logs()
10631066

10641067
assert logs is not None
10651068
assert hasattr(logs, "logs")
10661069
assert isinstance(logs.logs, list)
1070+
log_content = " ".join(str(log) for log in logs.logs)
1071+
assert test_message in log_content
10671072

10681073
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
10691074
def test_logs_with_execution_filter(self, shared_devbox: Devbox) -> None:
10701075
"""Test retrieving devbox logs filtered by execution ID."""
1071-
# Run a command and get its execution ID
1072-
execution = shared_devbox.cmd.exec_async('echo "filtered log test"')
1073-
result = execution.result()
1076+
test_message = "filtered log test"
1077+
result = shared_devbox.cmd.exec(f'echo "{test_message}"')
10741078
assert result.exit_code == 0
10751079

1076-
# Fetch logs filtered by execution ID - verifies API accepts the filter
1077-
logs = shared_devbox.logs(execution_id=execution.execution_id)
1080+
logs = shared_devbox.logs(execution_id=result.execution_id)
10781081

10791082
assert logs is not None
1083+
assert hasattr(logs, "logs")
10801084
assert isinstance(logs.logs, list)
1085+
log_content = " ".join(str(log) for log in logs.logs)
1086+
assert test_message in log_content
10811087

10821088
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
10831089
def test_logs_with_shell_name_filter(self, shared_devbox: Devbox) -> None:
10841090
"""Test retrieving devbox logs filtered by shell name."""
10851091
shell_name = "test-logs-shell"
10861092
shell = shared_devbox.shell(shell_name)
10871093

1088-
# Run a command in the named shell
1089-
result = shell.exec('echo "shell log test"')
1094+
test_message = "shell log test"
1095+
result = shell.exec(f'echo "{test_message}"')
10901096
assert result.exit_code == 0
10911097

1092-
# Fetch logs filtered by shell name - verifies API accepts the filter
10931098
logs = shared_devbox.logs(shell_name=shell_name)
10941099

10951100
assert logs is not None
1101+
assert hasattr(logs, "logs")
10961102
assert isinstance(logs.logs, list)
1103+
log_content = " ".join(str(log) for log in logs.logs)
1104+
assert test_message in log_content

0 commit comments

Comments
 (0)