Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/v6/guides/running-an-eval.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ change to `env.py` or the tasks:

| Runtime | Where the env runs |
| --- | --- |
| `LocalRuntime("env.py")` | A child process on your machine |
| `LocalRuntime("env.py")` | In this process, on your machine |
| `DockerRuntime("my-env")` | A fresh local container per rollout |
| `ModalRuntime("my-env")` | A fresh [Modal](https://modal.com) sandbox per rollout |
| `DaytonaRuntime("my-env")` | A fresh [Daytona](https://daytona.io) sandbox per rollout |
Expand Down
2 changes: 1 addition & 1 deletion docs/v6/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ For a platform taskset, pass its name or id directly: `hud eval "My Tasks" claud
| `--config`, `-c` | Agent config `key=value` (repeatable). |
| `--verbose`, `-v` | Show agent logs (step progress, tool calls) for batch runs too. |
| `--very-verbose`, `-vv` | Debug-level logs. |
| `--runtime` | Placement: `local`, `hud` (HUD runtime tunnel), or `tcp://host:port`. Defaults to `local` for a tasks file; platform tasksets default to remote hosted execution. |
| `--runtime` | Placement: `local` (a child process per rollout, serving the env source — `SubprocessRuntime`), `hud` (HUD runtime tunnel), or `tcp://host:port`. Defaults to `local` for a tasks file; platform tasksets default to remote hosted execution. |
| `--remote` | Run the whole rollout remotely on the HUD platform. |
| `--yes`, `-y` | Skip confirmation prompt. |

Expand Down
2 changes: 1 addition & 1 deletion docs/v6/reference/environment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ and [Tasks & Tasksets](/v6/reference/tasks).
You rarely serve by hand - `hud eval`, [`task.run()`](/v6/reference/tasks), and `Taskset.run()` bring
the environment up for you, and the [runtime](/v6/reference/runtime) you pass decides where. Serving
itself belongs to `hud.environment.server`, the entry point every substrate runs: a
[`LocalRuntime`](/v6/reference/runtime#localruntime) child process, a container CMD, or `hud serve`.
[`SubprocessRuntime`](/v6/reference/runtime#subprocessruntime) child process, a container CMD, or `hud serve`.

| Function | Description |
|----------|-------------|
Expand Down
43 changes: 37 additions & 6 deletions docs/v6/reference/runtime.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ await taskset.run(agent, runtime=LocalRuntime("env.py")) # serve env.py locall

| Runtime | Where the env runs | When to reach for it |
|---------|--------------------|----------------------|
| `LocalRuntime("env.py")` | A child process from your source | Fastest iteration; local development |
| `LocalRuntime("env.py")` | This process, loaded fresh from your source per rollout | Fastest iteration; local development |
| `SubprocessRuntime("env.py")` | A child process from your source | Local runs isolated from your process |
| `DockerRuntime("my-env")` | A fresh local container per rollout | Reproducibility and parity with production |
| `ModalRuntime("my-env")` | A fresh [Modal](https://modal.com/) sandbox per rollout | Cloud scale, no infra to manage |
| `DaytonaRuntime("my-env")` | A fresh [Daytona](https://www.daytona.io/) sandbox per rollout | Cloud scale on Daytona |
Expand All @@ -30,10 +31,17 @@ Most runtimes are on the top-level package (`from hud import LocalRuntime, Docke
HostedRuntime, Runtime`); `ModalRuntime` and `DaytonaRuntime` import from `hud.eval`.

<Note>
**Omit `runtime=` and it's inferred** from each task's `_source`, the file its template was defined
in. When every task shares one `_source`, that source is served locally as `LocalRuntime(source)`;
otherwise (mixed sources, or rows loaded from a file or the platform with no source) it falls back to
`HUDRuntime()`. Pass a runtime explicitly the moment you want something else.
**You can usually omit `runtime=`.** A run without one uses what HUD already knows:

- a taskset loaded from Python source (`Taskset.from_file` / `from_module`) runs against that source
- a platform taskset (`Taskset.from_api`) runs on the platform
- otherwise, if the envs your tasks name are defined in files you've imported, each rollout gets a
fresh env served from its file — so `my_task().run(agent)` just works in the project that defines
the env

When none of these apply, `run` raises and lists the runtimes you can pass — it never silently picks
one. If two imported files define an env with the same name, that's also an error; disambiguate by
passing the instance you mean: `runtime=LocalRuntime(env)`.
</Note>

To deploy an environment to the platform and run against it, see
Expand Down Expand Up @@ -74,7 +82,30 @@ The constructor for each built-in runtime:
### `LocalRuntime`

```python
LocalRuntime(path, *, env=None, ready_timeout=120.0)
LocalRuntime(source, *, env=None, ready_timeout=120.0)
```

Serves a fresh env per rollout, in this process, over the same control channel as every placement.
`source` is any pointer to the env:

- **a `.py` file or directory** that declares it, imported fresh per rollout (sibling imports
resolve). **`env`** pins one name when the source declares several; it defaults to the placed
task's env.
- **a live `Environment`** declared at module level - its declaring module's file is the recipe;
the instance itself is never served, so every rollout is still fresh.
- **a `(task) -> Environment` constructor** for envs built in code (integrations, parameterized
envs), called fresh per rollout with the placed row.

`ready_timeout` bounds `@env.initialize` startup. The freshness boundary is the env's own source:
it is re-imported per rollout, while modules it imports follow normal Python import caching and are
shared process-wide - state kept in helper modules persists across rollouts. Env hooks run in this
process and share its event loop - keep envs async, or use `SubprocessRuntime` / `DockerRuntime`
when rollouts need whole-process isolation.

### `SubprocessRuntime`

```python
SubprocessRuntime(path, *, env=None, ready_timeout=120.0)
```

- **`path`** - `.py` file (or directory) that declares the env. The child's working directory is the source's directory, so sibling imports and relative data paths resolve.
Expand Down
2 changes: 1 addition & 1 deletion docs/v6/start/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ hud eval env.py claude --runtime hud # same env, executed on HUD's hosted inf
```python
from hud.eval import LocalRuntime, DockerRuntime, ModalRuntime, HUDRuntime

LocalRuntime("env.py") # local child process - fastest iteration
LocalRuntime("env.py") # in this process - fastest iteration
DockerRuntime("my-env") # a fresh container per rollout
ModalRuntime("my-env") # a Modal cloud sandbox per rollout
HUDRuntime() # HUD's hosted infra (after `hud deploy`)
Expand Down
2 changes: 2 additions & 0 deletions hud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
RuntimeGPU,
RuntimeLimits,
RuntimeResources,
SubprocessRuntime,
SyncPlan,
Task,
Taskset,
Expand All @@ -49,6 +50,7 @@
"RuntimeGPU",
"RuntimeLimits",
"RuntimeResources",
"SubprocessRuntime",
"SyncPlan",
"Task",
"Taskset",
Expand Down
6 changes: 3 additions & 3 deletions hud/cli/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ def _python_defines_environment(path: Path) -> bool:


def _spawn_target(source: Path) -> Path:
"""The path the ``LocalRuntime`` provider serves.
"""The path the ``SubprocessRuntime`` provider serves.

Directories and env-defining ``.py`` files are served as-is. Task-only
sources (``tasks.py`` importing from ``env.py``) resolve to a sibling
Expand Down Expand Up @@ -736,15 +736,15 @@ def _resolve_placement(cfg: EvalConfig, source_path: Path | None) -> Any:
``--remote`` submits every rollout for platform-hosted execution; a
``tcp://`` url attaches to an env served elsewhere.
"""
from hud.eval import HostedRuntime, HUDRuntime, LocalRuntime, Runtime
from hud.eval import HostedRuntime, HUDRuntime, Runtime, SubprocessRuntime

if cfg.remote:
require_api_key("run remote hosted evals")
return HostedRuntime()
if cfg.runtime == "local":
if source_path is None:
raise ValueError("local placement requires a local source path")
return LocalRuntime(_spawn_target(source_path))
return SubprocessRuntime(_spawn_target(source_path))
if cfg.runtime == "hud":
require_api_key("run HUD runtime tunnel evals")
return HUDRuntime()
Expand Down
4 changes: 2 additions & 2 deletions hud/cli/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def _resolve(
"""
from contextlib import nullcontext

from hud.eval.runtime import LocalRuntime, Runtime
from hud.eval.runtime import Runtime, SubprocessRuntime

attach = url
if attach is None and source is None:
Expand All @@ -118,7 +118,7 @@ def _resolve(
hud_console.error(f"No task matching {task!r} (available: {available})")
raise typer.Exit(1)
selected = matches[0]
placement = LocalRuntime(_spawn_target(source or "."))(selected)
placement = SubprocessRuntime(_spawn_target(source or "."))(selected)
return selected.id, args or selected.args, placement


Expand Down
8 changes: 1 addition & 7 deletions hud/environment/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,7 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> EvalTask:
from hud.eval.task import Task

bound = self.sig.bind(*args, **kwargs)
task = Task(env=self.env.name, id=self.id, args=dict(bound.arguments))
# Record where this template was defined so ``task.run()`` can default to
# serving that source locally (in-process only; never crosses the wire).
source = inspect.getsourcefile(self.func)
if source is not None:
task._source = source
return task
return Task(env=self.env.name, id=self.id, args=dict(bound.arguments))


class Environment(LegacyEnvMixin):
Expand Down
2 changes: 1 addition & 1 deletion hud/environment/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
(:func:`bind`), and the full serving lifecycle (:func:`serve`) — backing
daemons up, control channel bound (announcing the port on stdout as
``HUD_SERVE_PORT=<port>``), daemons down. Every substrate shape runs it: the
:class:`~hud.eval.runtime.LocalRuntime` child process, a container CMD, and
:class:`~hud.eval.runtime.SubprocessRuntime` child process, a container CMD, and
``hud serve``.
"""

Expand Down
8 changes: 5 additions & 3 deletions hud/eval/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
row.)

Placement is passed at execution time (see :mod:`.runtime`): ``LocalRuntime`` a
local source, ``DockerRuntime`` an image, ``Runtime(url)`` an env served
elsewhere, ``HUDRuntime`` a HUD runtime tunnel, or ``HostedRuntime`` to run the
whole rollout remotely on the platform::
local source served in-process, ``DockerRuntime`` an image,
``Runtime(url)`` an env served elsewhere, ``HUDRuntime`` a HUD runtime tunnel,
or ``HostedRuntime`` to run the whole rollout remotely on the platform::

from hud.eval import LocalRuntime, Taskset

Expand Down Expand Up @@ -46,6 +46,7 @@
RuntimeGPU,
RuntimeLimits,
RuntimeResources,
SubprocessRuntime,
)
from .sync import SyncPlan
from .task import Task
Expand All @@ -68,6 +69,7 @@
"RuntimeGPU",
"RuntimeLimits",
"RuntimeResources",
"SubprocessRuntime",
"SyncPlan",
"Task",
"Taskset",
Expand Down
5 changes: 3 additions & 2 deletions hud/eval/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,9 @@ async def send(self, message: MessageContent) -> Trace:
)
if self._runtime is None:
raise RuntimeError(
"Chat needs a runtime to converse against — pass an env placement, "
'e.g. runtime=Runtime("tcp://...") or runtime=LocalRuntime("env.py").'
"Chat needs a runtime to converse against — pass an env placement: "
'LocalRuntime("env.py") (a source file), LocalRuntime(env) (a live env), '
"Runtime(url) (a served substrate), or HUDRuntime() (your deployed env)."
)
if self.job is None: # one job spans the whole conversation
self.job = await Job.start(self._task.id)
Expand Down
Loading
Loading