Repository navigation
feat(cli): task run and eval run always go through the configured [runner] - #236
Open
AlexS-scale wants to merge 30 commits into
Open
AlexS-scale wants to merge 30 commits into
AlexS-scale wants to merge 30 commits into
Conversation
…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.
…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
…s left up for its submitter
… failures it re-runs
…, and a saved context's keep and start_step yield to the command's
…ep output from the step's journal entry
… 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.
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
…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.
earakely-scale
approved these changes
Oct 19, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
task runandeval runnow always hand their runs to the configured[runner]and follow them. There is no--submitflag, so the same commands work whether the runner isLocalRunner(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=...)andKEEP_KEY), both onmainnow; this diff is the CLI change and the runner fixes it needed.Every option keeps working, with the same default under any runner:
--gateway-env-id,--service-db-env-id, and the other per-run overridesuser_overridesin the run's metadata (#255)--context-json+--start-stepinstance_idwithstart_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-stepand--keepalways win over values saved in the file.--keepmetadata[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)Following a run. Progress comes from the run index and the task instance, the records every runner keeps.
verifier_id.task runprints each step's output, replayed from that step's own journal entry.eval runprint each step and each verifier step's score.task runcan't write to--output-diralso exits 1, after teardown and--keep.runner.start(), and always awaitsstop()unless a second Ctrl-C stops waiting.Runner changes this needed:
new_instance_id, the conventioncreate_instancealready used), forLocalRunner.submitand the CLI's pre-store. An@localtask's run, and ids derived from it such as collected artifacts, stay in the@localnamespace. Explorer-submitted runs get such ids too; the explorer already URL-encodes instance ids.LocalRunnerrun records the process that owns it (host and pid), which marks it seen everyOWNER_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 withinOWNER_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).LocalRunneris never started.Behaviour change: concurrency is the runner's everywhere. Under the default
LocalRunnerthat is[runner.config] workers(2), where the in-process CLI used to start every--krun at once.Limitations:
task run-batch, bundleagent-env runand the validators still run in-process.--start-step, its seeded earlier steps are echoed as completed.Tests:
task run/eval runCLI tests go throughLocalRunner.task_run_stored_test.pyruns real stored tasks through the real stores, all through the actual CLI:@localtask, fresh and continued from--context-json, writes no run data to the configured store.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--keeprun another runner tried to cancel completes and is held and torn down with its sibling.

Confidence Score: 5/5
The PR appears safe to merge; the remaining reported cleanup issue is addressed.Summary
task runandeval runsubmit through the configured runner and follow stored progress.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]Reviews (13) · Last reviewed commit: "fix(cli): a failed status read doesn't h..." · Reviewed by Greptile