Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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