Skip to content

feat(cli): task run and eval run always go through the configured [runner] - #236

Open
AlexS-scale wants to merge 30 commits into
mainfrom
AlexS/cli-submit-through-runner
Open

AlexS-scale wants to merge 30 commits into
mainfrom
AlexS/cli-submit-through-runner

Conversation

@AlexS-scale

@AlexS-scale AlexS-scale commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

task run and eval run now always hand their runs to the configured [runner] and follow them. There is no --submit flag, so the same commands work whether the runner is LocalRunner (the default, in this process) or an external one such as a Temporal or LittleHorse plugin.

Builds on #255 (per-run overrides) and #256 (submit(instance_id=...) and KEEP_KEY), both on main now; this diff is the CLI change and the runner fixes it needed.

Every option keeps working, with the same default under any runner:

Option How it reaches the runner
--gateway-env-id, --service-db-env-id, and the other per-run overrides user_overrides in the run's metadata (#255)
--context-json + --start-step The CLI stores the file's context as a new instance, within the task's run scope, and submits that instance_id with start_step; if that write fails, the command fails before anything is submitted. The runner continues it from the stored record (#256). The command's --start-step and --keep always win over values saved in the file.
--keep metadata[KEEP_KEY]. The runner leaves a run that ended on its own up (#256). The CLI prints it, holds it until Ctrl-C and tears it down from the stored instance; if following the runs fails, it tears it down before reporting the error. A run Ctrl-C cancels is torn down by the runner.
--max-concurrency (eval) The CLI keeps at most that many of its runs in flight. The runner's own capacity still applies.

Following a run. Progress comes from the run index and the task instance, the records every runner keeps.

  • A step is shown as the run ran it, with the instance's seed substituted into its id and verifier_id.
  • A single task run prints each step's output, replayed from that step's own journal entry.
  • Parallel runs and eval run print each step and each verifier step's score.
  • Exit codes are unchanged: 130/143 on Ctrl-C/SIGTERM, 1 if a run failed. A run context task run can't write to --output-dir also exits 1, after teardown and --keep.
  • The CLI never calls runner.start(), and always awaits stop() unless a second Ctrl-C stops waiting.

Runner changes this needed:

  • Instance ids. A run's instance id derives from its task id (new_instance_id, the convention create_instance already used), for LocalRunner.submit and the CLI's pre-store. An @local task's run, and ids derived from it such as collected artifacts, stay in the @local namespace. Explorer-submitted runs get such ids too; the explorer already URL-encodes instance ids.
  • Run ownership. Each LocalRunner run records the process that owns it (host and pid), which marks it seen every OWNER_SEEN_SECONDS (30) while it runs. start(), and a started runner on each tick after it, fails only a run whose owner is gone: on this host, its pid has exited; on another, it hasn't seen the run within OWNER_LEASE_SECONDS (120), so a hub back under a new hostname still reconciles its old runs. A run that names no owner is an orphan too. Opening the explorer while a CLI run is going no longer marks that run FAILED. cancel() only cancels a run this runner is running; for any other it returns False and leaves the record to its owner, so the explorer can no longer mark a CLI's run CANCELED while it goes on (which, under --keep, left its sandbox up).
  • Run index. The run index makes its indexes on first use, since a CLI-hosted LocalRunner is never started.

Behaviour change: concurrency is the runner's everywhere. Under the default LocalRunner that is [runner.config] workers (2), where the in-process CLI used to start every --k run at once.

Limitations:

  • task run-batch, bundle agent-env run and the validators still run in-process.
  • A pid the OS has reused looks alive, so a run that owner left stays in flight until that pid exits.
  • When a run resumes at --start-step, its seeded earlier steps are echoed as completed.

Tests:

  • The task run / eval run CLI tests go through LocalRunner.
  • task_run_stored_test.py runs real stored tasks through the real stores, all through the actual CLI:
    • An @local task, fresh and continued from --context-json, writes no run data to the configured store.
    • Seeded step ids and verifier ids report correctly at k=1 and k=2.
    • An unexportable context fails the command, and a failed run's own failure is preserved.
  • Two LocalRunners sharing a store: start() spares the other's live run and fails a dead process's. A cancel through the runner not running a run returns False and leaves it running, and the owner's cancel still stops and tears it down. Through the CLI, a --keep run another runner tried to cancel completes and is held and torn down with its sibling.
  • Each of these fails on 52022bf and passes now.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; the remaining reported cleanup issue is addressed.

Summary

task run and eval run submit through the configured runner and follow stored progress.

  • Task and eval commands submit runs to the configured runner and follow them.
  • Task runs resume from stored context and can keep their sandboxes up.
  • LocalRunner tracks run owners and only cancels runs it owns.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Submit runs] --> B[Poll each unfinished run]
  B --> C[Save progress and collect read errors]
  C --> D{Any read failed?}
  D -->|No| E{All runs finished?}
  E -->|No| B
  E -->|Yes| F[Await runner.stop]
  D -->|Yes| F
  F --> G[Export results or report error]
  G --> H[Hold or tear down finished kept runs]
Loading

Reviews (13) · Last reviewed commit: "fix(cli): a failed status read doesn't h..." · Reviewed by Greptile

…nner]

Only the explorer dispatched through the [runner]; task run and eval run always ran
Task.run() in their own process, so an out-of-process runner never governed them.

--submit submits each run through get_runner(), follows it from the run index and
its task instance (status changes and completed steps), writes each instance's
context to --output-dir and exits 1 when a run did not complete. The first Ctrl-C
asks the runner to cancel the runs still going and follows them to the end; a second
stops waiting. The command never calls start(), which belongs to the serving process:
LocalRunner's fails every local run another process has in flight. It calls stop(),
which lets a LocalRunner finish tearing its runs down.

Options that act on this process only (--keep, --context-json, --gateway-env-id,
--service-db-env-id, eval's --max-concurrency) are refused with --submit. Without it
both commands behave as before.
@AlexS-scale
AlexS-scale requested a review from a team as a code owner October 16, 2026 19:06
Comment thread src/agent_env/cli/submitted_runs.py Outdated
Comment thread src/agent_env/cli/submitted_runs.py Outdated
…trl-C

An error while submitting or following skipped runner.stop(), so a LocalRunner's
runs lost their teardown when the loop closed; and a first Ctrl-C during the final
stop() ended it early. Every path short of a second Ctrl-C now awaits stop(), marked
as tearing down so the first signal spares it.
An exception from cancel() or the follow-up status reads, raised inside the
cancellation handler, escaped the except Exception clause. stop() now runs in a
finally that only a second Ctrl-C skips.
…ugh-runner

# Conflicts:
#	src/agent_env/cli/eval/run.py
#	src/agent_env/cli/task/run.py
@AlexS-scale AlexS-scale changed the title feat(cli): --submit hands task run and eval run to the configured [runner] feat(cli): task run and eval run always go through the configured [runner] Oct 19, 2026
Comment thread src/agent_env/cli/task/run.py Outdated
Comment thread src/agent_env/runner/local_runner.py Outdated
Comment thread src/agent_env/cli/task/run.py Outdated
Comment thread src/agent_env/cli/eval/run.py Outdated
Comment thread src/agent_env/cli/eval/run.py Outdated
… run stays local

LocalRunner.submit() and the CLI's --context-json pre-store minted bare UUID instance ids. Ids a step derives
from the instance (collect_artifacts' artifact ids) then lost the @Local namespace and the run failed with
LocalRunWriteError, and the pre-store's seed journal, keyed only by that instance id, landed in the configured
store. Both now use new_instance_id(task_id), the convention create_instance already followed, and the CLI
pre-stores within the task's run scope.
…cess has exited

start() failed every non-terminal local run as an orphan of a previous process. With task run and eval run
going through LocalRunner, opening the explorer while a CLI run was going marked that run FAILED ("interrupted by
a hub restart"), and the CLI then cancelled it. Each run now records the process that submitted it (host and
pid); start() fails a run whose owner on this host has exited, or that names no owner, and leaves the rest.
…it ran with

Task.run() substitutes a run's seed into its steps, so a step "verify-<week>" runs as "verify-3". Following the
run looked steps up in the stored task's definitions, so the lookup raised StopIteration (surfacing as
"coroutine raised StopIteration") and a templated verifier_id lost its score. The follower now resolves the
definitions with the instance's seed, as Task.run() does, before matching and rendering a step.
With an unwritable --output-dir, task run printed the write error, then "Task completed!", and exited 0 without
the JSON it was asked for; the in-process command used to exit nonzero. A failed export now suppresses the
success line and exits 1 once the runs are torn down and --keep has been handled. A run that failed still
reports its own failure as before.
@AlexS-scale AlexS-scale changed the title feat(cli): task run and eval run always go through the configured [runner] feat(cli)!: task run and eval run always go through the configured [runner] Oct 19, 2026
Comment thread src/agent_env/runner/local_runner.py Outdated
A run missing from this runner's in-flight tasks was marked CANCELED and reported cancelled. The explorer
reaches that branch for a run a CLI process is running, so the run went on while its record said CANCELED.
Under task run --keep, a run cancelled that way that then ended was neither torn down by its runner (it was kept)
nor by the CLI (its record said cancelled), and its sandbox was left up. cancel() now returns False, and leaves
the record alone, when this runner has no live task for the run.
…ugh-runner

# Conflicts:
#	src/agent_env/cli/task/run.py
#	src/agent_env/runner/local_runner.py
@AlexS-scale AlexS-scale changed the title feat(cli)!: task run and eval run always go through the configured [runner] feat(cli): task run and eval run always go through the configured [runner] Oct 19, 2026
Comment thread src/agent_env/cli/task/run.py Outdated
Comment thread src/agent_env/cli/task/run.py Outdated
…ore anything is submitted

The pre-store went through register_task_instance and seed_task_instance_context, which log a failed write and
carry on, so a run could be submitted to continue an instance with an empty or partial context. It now writes
through the task instance store directly, so a failed write raises before any run is submitted.
…that ended left up

If following raised, for example on a failed status read, after one run of task run --keep --k 2 had ended,
the command left before settling what --keep held. Its runner had skipped that run's teardown because it was
kept, so its sandbox stayed up. follow() now fills a list the caller holds, and on an error task run tears
down what the runs that ended on their own left up before it re-raises.
…st is reconciled too

_owner_alive() counted every owner on another host as alive, so a hub that came back under a new hostname (a new
container on the same persistent store) never reconciled its old runs: they stayed QUEUED or RUNNING. Each
LocalRunner now marks its runs seen every OWNER_SEEN_SECONDS, a CLI-hosted one included. An owner on another host
counts as alive only while it saw the run within OWNER_LEASE_SECONDS, and a started runner reconciles on the same
tick, so a run its owner stopped seeing is failed once the lease runs out. An owner on this host is still judged by
its pid.
…ater in the same poll

The poll reads every run before raising the first failed read, so the run is seen as ended and its kept sandbox is torn down.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants