Metadata-Version: 2.4
Name: vswarm
Version: 0.1.9
Summary: Async Python SDK for the VSwarm runtime platform
Author: Akshay Painjane
License-Expression: Apache-2.0
Project-URL: Homepage, https://pypi.org/project/vswarm/
Project-URL: Documentation, https://pypi.org/project/vswarm/#description
Project-URL: Changelog, https://pypi.org/project/vswarm/#history
Keywords: agents,browser-automation,runtime,sandbox,vswarm
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: httpx<1,>=0.26
Requires-Dist: packaging<27,>=24
Provides-Extra: pty
Requires-Dist: websockets<17,>=15; extra == "pty"
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
Dynamic: license-file

# VSwarm Python SDK

The VSwarm Python SDK provides typed, asynchronous access to VSwarm workspaces,
sandboxes, processes, browser sessions, traces, artifacts, and runtime APIs.

Install the published SDK from PyPI:

```bash
pip install vswarm
```

For local VSwarm monorepo development, install this SDK in editable mode:

```bash
pip install -e "sdks/python[test]"
```

The Python SDK is distributed under the Apache License 2.0. That license applies
to this SDK distribution; it does not change the licensing or visibility of the
VSwarm server, gateway, deployment, or other private monorepo components.

Recommended browser sandbox flow:

```python
from vswarm import Admission, BrowserPolicy, RequestOptions, Viewport, VSwarmClient

async with VSwarmClient(base_url="http://127.0.0.1:8080", token="secret") as client:
    async with client.browser.session(
        template="browser-chromium",
        policy=BrowserPolicy(allowed_domains=["example.com"]),
        viewport=Viewport(width=1440, height=900),
        admission=Admission.fail_fast(),
    ) as browser:
        obs = await browser.observe_for_agent(screenshot=True, compact_elements=True)

        await browser.click(
            obs.element(text="Go"),
            observe_after=True,
            observation={"include": {"screenshot": True, "dom": True}},
        )
```

This is the primary SDK UX: the SDK asks VSwarm to create an owned browser session, the server creates or claims the sandbox, and cleanup is automatic on success, error, or cancellation. If the server has a compatible warm `browser-chromium` sandbox available, this path can avoid cold container startup. `client.browser_session(...)` remains as a compatibility alias for the same owned workflow.

Production services that create a session in one request and destroy it later can use detached owned
session creation while still relying on SDK cleanup semantics:

```python
browser = await client.browser.create_session(
    template="browser-chromium",
    policy=BrowserPolicy(allowed_domains=["example.com"]),
    viewport=Viewport(width=1440, height=900),
    admission=Admission.queue(timeout_seconds=30),
    options=RequestOptions(request_id="agent-task-create-123"),
)

try:
    obs = await browser.observe_for_agent(
        options=RequestOptions(request_id="agent-task-observe-123"),
    )
finally:
    await browser.cleanup(missing_ok=True)
```

`RequestOptions.request_id` sets `X-VSwarm-Request-ID` for correlation. It does not imply
idempotency.

Workspace and sandbox creation support explicit, persisted idempotency keys. Reusing a key
with equivalent normalized input returns the original resource with `idempotent_replay=true`;
reusing it with different input raises `VSwarmConflictError` with code
`idempotency_conflict`. This makes a caller-controlled retry after an ambiguous transport
failure safe:

```python
from vswarm import SandboxCreateRequest, WorkspaceCreateRequest

async with VSwarmClient(
    base_url="http://vswarm.internal:8080",
    token="secret",
    connect_timeout=10,
    timeout=60,
) as client:
    workspace = await client.workspaces.create(
        WorkspaceCreateRequest(
            client_action_id="my-service:operation-123:workspace",
            source={"type": "empty"},
        )
    )
    sandbox = await client.sandboxes.create(
        SandboxCreateRequest(
            client_action_id="my-service:operation-123:sandbox",
            template="base-ubuntu",
            workspace={"id": workspace.id, "mount_path": "/workspace", "mode": "read_write"},
        )
    )
```

Product backends that act for an end user must use a separate delegation
credential. Configure the server with `VSWARM_DELEGATED_ACTOR_TOKEN`, keep that
secret out of browsers, and pass it alongside `project` and `user`. This
hardened delegation boundary requires VSwarm Python SDK `0.1.3` or newer:

```python
client = VSwarmClient(
    base_url="http://vswarm.internal:8080",
    token="api-secret",
    delegated_actor_token="product-backend-secret",
    project="project-1",
    user="user-1",
)
```

VSwarm rejects actor headers that are not accompanied by the delegation
credential; possession of the general API token alone never grants identity
impersonation.

The SDK does not retry creates automatically. Callers should retry only operations carrying a
stable idempotency key, and should keep retry counts bounded.

Advanced lifecycle control is still available when a caller needs to attach browser sessions to a long-lived sandbox:

```python
sandbox = await client.sandboxes.create(
    SandboxCreateRequest(template="browser-chromium")
)
browser = await sandbox.browser.sessions.create(policy=BrowserPolicy(block_localhost=True))
try:
    obs = await browser.observe()
    await browser.action("click", target={"selector": "button"})
finally:
    await browser.close()
    await sandbox.destroy()
```

One-shot run execution uses the canonical runs namespace:

```python
result = await client.runs.execute(
    template="code-runner-multilang",
    profile="python",
    files={"/workspace/main.py": "print(input())"},
    stdin="hello",
    timeout_seconds=20,
)
```

Run-scoped trace context is available through the canonical traces namespace and the `client.trace(...)` async context manager:

```python
async with VSwarmClient(base_url="http://127.0.0.1:8080", token="secret") as client:
    async with client.trace(name="browser-login-task", capture_profile="debug", client_run_id="eval-42") as trace:
        await trace.event("client.agent.step.started", {"step": "open-login"}, step_id="step-open-login")

        async with client.browser.session(template="browser-chromium", trace=trace) as browser:
            obs = await browser.observe_for_agent()
            await browser.click(obs.element(text="Sign in"), observe_after=True)

        await trace.download_bundle("vswarm-trace-browser-login-task.zip")
```

While a trace context is active, the SDK propagates `X-VSwarm-Trace-ID`, `X-VSwarm-Client-Run-ID`, and optional step/parent headers on VSwarm calls. Client-side events stay under `client.*` ownership and are stored alongside VSwarm runtime/browser/artifact events in the same ordered trace.

Available SDK surface:

- `client.system.info()`
- `client.require_contract(...)`
- `VSwarmClient.browser_capacity()`
- `VSwarmClient.reconcile_browser()`
- `client.browser.session(...)`
- `client.browser.create_session(...)`
- `client.browser.capacity()`
- `client.browser.reconcile()`
- `client.browser.attach(...)`
- `client.browser_session(...)`
- `client.runs.execute(...)`
- `client.trace(...)`
- `client.traces.create/list/get/events/artifacts/export/download_bundle(...)`
- `client.templates.list()` and `client.templates.get(name)`
- `client.sandboxes.create(...)`, `get(id)`, and `destroy(id)`
- `client.runtime_leases.create/list/get/reconcile(...)`
- runtime lease `renew/activity/checkpoint/hibernate/resume` methods and typed variants
- `client.snapshots.create/list/get/restore(...)` and typed variants
- `sandbox.exec(...)`
- `sandbox.files.list/read/write(...)`
- `sandbox.browser.sessions.create(...)` and `sandbox.browser.create_session(...)`
- typed `browser.observe(...)` returning `BrowserObservation`
- `browser.observe_for_agent(...)`
- typed helpers: `browser.navigate`, `click`, `double_click`, `hover`, `fill`, `type`, `press`, `scroll`, `drag`, `select_option`, `wait`, `get_text`, `find_text`, `extract_page`, `extract_section`, `capture_page`, `query_elements`, `screenshot`, `upload_file`, `evaluate`
- typed `browser.act(...)` for browser action objects
- raw `browser.action(...)` escape hatch
- `browser.batch(...)`
- `browser.actions()` and `browser.observations()`
- `browser.cleanup(missing_ok=True)`, `browser.close(missing_ok=True)`, and public `browser.sandbox_id` / `browser.session_id`
- `observation.compact_elements()` and `observation.element(...)`
- `browser.latest_screenshot_base64(observation)`
- `client.artifacts.get(id)`, `download_bytes(id)`, and `download_base64(id)`

### Workspace capability contract

`await client.system.info()` returns the server's typed version and enabled
feature set. Applications that depend on a specific runtime contract should
call `await client.require_contract(...)` explicitly during startup.

Workspace resources expose one canonical method per operation. Those methods
return detached, recursively immutable response models; the former raw mapping
methods and `_typed` twins are not part of the 0.1.5 API.

### Runtime leases and snapshots

A workspace is durable state and may exist with zero live compute. A runtime
lease is the authoritative allocation of live compute for that workspace.
Checkpoint commits an immutable snapshot without releasing compute; hibernate
quiesces runtime resources, commits the snapshot, and only then releases
compute. Resume restores into a new runtime generation:

```python
from vswarm import RuntimeActionRequest, RuntimeCreateRequest, WorkspaceCreateRequest

workspace = await client.workspaces.create(
    WorkspaceCreateRequest(source={"type": "empty"})
)
lease = await workspace.create_runtime_lease(
    RuntimeCreateRequest(
        template="base-ubuntu",
        client_action_id="task-42:create-runtime",
        idle_ttl_seconds=1800,
        hard_ttl_seconds=28800,
    )
)

snapshot = await lease.checkpoint(
    RuntimeActionRequest(
        client_action_id="task-42:checkpoint",
        expected_runtime_generation=lease.model.runtime_generation,
    )
)
hibernated = await lease.hibernate(
    RuntimeActionRequest(
        client_action_id="task-42:hibernate",
        expected_runtime_generation=lease.model.runtime_generation,
    )
)
resumed = await lease.resume(
    RuntimeActionRequest(client_action_id="task-42:resume")
)
```

Use a stable client action ID only for retries of the same normalized request.
Changing a request while reusing an action ID returns
`idempotency_conflict`. Snapshot restore verifies both archive and manifest
checksums before replacing workspace data. Old runtime-scoped resource IDs do
not control the new generation.

### Reconnecting to a PTY

Install the optional stream dependency with `pip install "vswarm[pty]"`. The
stream descriptor keeps the API bearer credential in WebSocket headers rather
than adding it to the URL:

```python
sandbox = await client.sandboxes.get("sbx_123")
stream = await sandbox.pty.stream("pty_123", offset=last_offset)

async with stream:
    async for frame in stream:
        if frame["gap"]:
            print("terminal replay was truncated")
        consume(frame["data"])
        last_offset = frame["next_offset"]
```

The stream also exposes versioned `input`, `resize`, `signal`, and `close`
controls. Create a fresh stream with the last acknowledged offset after a
transport disconnect; a WebSocket disconnect does not terminate the
authoritative PTY session.

`ProcessClient.restart_group` performs an identity-preserving, generation-fenced,
idempotent server-side restart. The SDK parser supports runtime-state contract
version `4`, including aggregate `version` and
`runtime_generation_version` fields.

Request helpers build detached payloads and do not mutate caller-owned command,
intent, endpoint, metadata, or options mappings.

Runtime leases and snapshots are reported as supported in `workspace.v1`.
Read-only leases remain unavailable until every runtime backend can enforce a
read-only workspace mount. Worker administration, deployment administration,
and billing remain outside this SDK contract.

The default transport uses `httpx.AsyncClient` lazily. Tests and downstream apps can inject a fake async transport with a compatible `request(...)` method.

`browser.observe_for_agent(...)` is the bounded prompt/multimodal path. It sends server-side limits for `max_elements`, `max_dom_bytes`, and `max_screenshot_bytes`, requests compact elements, and only asks for screenshot base64 when `screenshot_base64=True`.

Browser actions return `BrowserActionResult` objects for page-state failures as well as successes. Recoverable failures such as `stale_element_ref`, `selector_not_found`, `action_timeout`, and `policy_blocked` are exposed through `result.code`, `result.message`, and `result.recoverable`.

## Admission and capacity

Browser sessions support typed admission options:

```python
async with client.browser.session(
    template="browser-chromium",
    admission=Admission.fail_fast(),
) as browser:
    ...
```

```python
async with client.browser.session(
    template="browser-chromium",
    admission=Admission.queue(timeout_seconds=30),
) as browser:
    ...
```

Capacity can be inspected before or after requests:

```python
capacity = await client.browser.capacity(template="browser-chromium")
print(capacity.available_slots)
print(capacity.raw.get("warm_pool_ready"))
```

Capacity failures expose retry metadata when the server provides it:

```python
from vswarm import VSwarmCapacityError

try:
    async with client.browser.session(admission=Admission.fail_fast()) as browser:
        ...
except VSwarmCapacityError as exc:
    print(exc.retry_after_seconds)
    print(exc.capacity)
```

Run the live local browser smoke test against a running VSwarm API:

```bash
PYTHONPATH=sdks/python python sdks/python/scripts/live_browser_smoke.py --api http://127.0.0.1:8080
```

## Compatibility and releases

Workspace file changes use the bounded durable SSE stream rather than polling:

```python
from vswarm import WorkspaceFileEventSubscribeRequest

request = WorkspaceFileEventSubscribeRequest(
    sandbox_id="sbx_123",
    runtime_generation="generation-4",
    after_sequence=17,
)
async for item in client.events.subscribe_workspace_files(request):
    print(item)
```

The iterator closes the response on completion, cancellation, and contract
failure. `EventStreamResyncRequired` tells consumers to refresh their bounded
workspace projection before resuming.

The SDK requires Python 3.10 or newer. The public API remains provisional while
the SDK is on the `0.x` release line, so consumers should pin an explicitly
tested range. Releases may remove provisional compatibility surfaces when the
replacement server/SDK contract is made mandatory; those removals are called
out in the changelog.

Use an explicit contract requirement when an application must fail closed
against an incompatible VSwarm server:

```python
from vswarm import ContractRequirement, VSwarmClient

async with VSwarmClient(base_url="https://api.example.com", token="secret") as client:
    await client.require_contract(ContractRequirement(
        minimum_api_version="0.2.0",
        runtime_state_contract_version="4",
        required_features=frozenset({"runtime_leases", "snapshots"}),
    ))
```

## License

The VSwarm Python SDK is licensed under the Apache License 2.0. This SDK license
does not apply to the private VSwarm server or other monorepo components.
