Skip to content

feat(task): read recorded runs through task_instances and find_task_instance - #124

Merged
earakely-scale merged 4 commits into
mainfrom
feat/task-instances-read-api
Oct 8, 2026
Merged

earakely-scale merged 4 commits into
mainfrom
feat/task-instances-read-api

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

A plugin that reads a task's recorded runs has no supported way to do it. agentenv-hf's agent-env hf publish, for one, runs a raw task_instances query through the document store, which is off the plugin surface and so internal. The explorer's instance routes run the same query.

agent_env.task.store gains three reads, added to the plugin surface:

from agent_env.task.store import count_task_instances, find_task_instance, task_instances

task_id = "@local/mycorp-demo/triage/route-ticket"
runs = task_instances(task_id)                                      # newest first
page = task_instances(task_id, task_version=2, limit=50, offset=50)
total = count_task_instances(task_id, task_version=2)
run = find_task_instance("@local/mycorp-demo/triage/route-ticket-7f3a9c2e")  # None when there is no such run
  • Return type: they return the existing TaskInstance and are backed by two new TaskInstanceStore methods (find, count) next to get.
  • Ordering: newest first by the start minute a run records, then by instance id descending, so paging with limit and offset neither repeats nor skips a run.
  • Indexes: the two compound task_instances indexes now end in instance_id: (task_id, created_at_utc, instance_id) and (task_id, task_version, created_at_utc, instance_id).
    • With both sort keys descending, Mongo reads the order from the index scanned backward, with no sort stage.
    • Their leading fields still serve the existing sorts on created_at_utc alone.
  • Explorer routes: GET /api/v1/tasks/{task_id}/instances and .../instances/{instance_id} use the new functions, which gives the reads their in-repo consumer. Their responses are now the TaskInstance fields, so the store's own bookkeeping fields (journal_seq, run_generation, rerecorded_steps) drop out. The UI reads none of them.
  • Plugin surface: check_plugin_api.py USED gains TaskInstance and the three functions, under "What a plugin reads of recorded runs".
  • Docs: docs: add CITATION.cff and link the README to the plugin docs #119 moved the plugin contract out of the README to the docs site, so this PR no longer touches the README. The usage is in the functions' docstrings; a "Recorded runs" section for the site's plugin or Python API docs can follow separately.
  • Unchanged: writes, TaskInstanceStore's other methods and the run-group routes. TaskInstanceStore itself stays off the surface, so its write methods aren't frozen.
  • Additive: nothing is renamed or removed, so the title has no !.

How it was tested

  • pytest tst/unit/task/task_instance_reads_test.py (new, 4 tests on a real LocalSqliteDocumentStore). They cover:
    • newest first across minutes, with other tasks' runs excluded;
    • the task_version filter with limit and offset, and counts;
    • find_task_instance, including None for an unknown id;
    • a run created by register_task_instance reading back.
  • tst/unit/explorer/explorer_app_test.py gains test_the_instance_routes_page_newest_first_and_answer_404_for_an_unknown_run. It covers newest-first paging with total and has_more, the version filter, the dropped bookkeeping field, a lookup and a 404. The existing run-route tests pass unchanged.
  • pytest tst/unit/test_check_plugin_api.py: every surface path, the new ones included, resolves in the checkout.
  • python .github/scripts/check_plugin_api.py --base origin/main --title "<this title>": "no break to the plugin surface".
  • make unit-test on the branch merged with main: 6858 passed, 13 skipped (the same skips as main).

Chaos testing

Two parallel runs of about 15 minutes each.

  • Store backends: randomised paging on SQLite, @local routing (SQLite with SQLite, and Mongo with SQLite), DynamoDB under moto and real MongoDB 7.
    • Many runs shared a minute, with mixed versions and look-alike task ids.
    • Every page set concatenated exactly to the full ordered list, and counts, version filters and lookups agreed on every backend.
  • MongoDB at scale: 2,000 runs of ~150 KB each, about 300 MB for one task.
    • This found a regression in the first tie-break: the mixed-direction sort matched no index. A page past the first sorted all 301 MB in memory, and failed with "Sort exceeded memory limit" when external sort was off.
    • The indexes above fix it. With external sort off, every page, including the explorer's largest from deep offsets, takes 0.02–0.2 s. Explain shows LIMIT <- FETCH <- SKIP <- IXSCAN(..., backward) for both the plain and the version-filtered query.
  • Explorer end to end: real agent-env run records (passing, scored-0, failed and cancelled runs) were served by the old and new explorer on the same store.
    • 144 requests match apart from the three dropped bookkeeping fields, the fixed tie order, and an empty step_attempt_failures on runs that lacked the key.
    • Edge parameters return the same status codes on both servers.
    • 772 requests made while 12 runs were being written all returned 200, with totals that only rose.
    • Headless Chrome rendered the task and run views with no JS errors.

Notes for review

  • Index build on upgrade: a store creates the two new compound indexes the first time a process uses it, as this file already did for the existing ones. On a large Mongo or Firestore collection, building them before the deploy keeps that first use fast.
  • Strict decoding: the explorer's two instance routes now decode records as TaskInstance, as agent-env task get-instance already does. A record that core never writes, such as one missing total_steps or with an unknown step status, now gets a 500 from those routes, where it used to be passed through raw.
    • Core has written all six required keys since the collection was introduced, and TaskStepStatus has only ever been success/failure.
    • So, per AGENTS.md, there's no defensive decoding. Say if you'd rather the decoder tolerate foreign records.

After release, agentenv-hf switches to these functions and raises its framework floor.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding finding or new issue was established.

Summary

The task store now offers typed reads for recorded runs, and Explorer uses them for its instance routes. This gives plugins a supported way to read runs and keeps the route responses to TaskInstance fields.

  • Plugins can read recorded task runs through the task store.
  • Explorer instance routes return typed task-run fields.

Reviews (4) · Last reviewed commit: "fix(task): serve the tie-broken run orde..." · Reviewed by Greptile

…nstance

Plugins that read a task's recorded runs (agentenv-hf's publish, for one) had only a raw task_instances query
through the document store, which the README calls internal. agent_env.task.store gains three reads on the plugin
surface: task_instances (a task's runs, newest first, with a task_version filter and limit/offset),
count_task_instances, and find_task_instance (None for an unknown id), returning the existing TaskInstance. The
explorer's instance list and lookup routes use them, which drops the store's own bookkeeping fields
(journal_seq, run_generation, rerecorded_steps) from those responses; the UI reads none of them. The README lists
the reads in the plugin surface and adds a Recorded runs section with usage, and check_plugin_api.py guards them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 8, 2026 18:25
Comment thread src/agent_env/task/store.py Outdated
earakely-scale and others added 3 commits October 8, 2026 11:32
…rlap

A run records only the minute it started, so task_instances sorted runs of one minute in whatever order the store
returned them, and a plugin paging with limit and offset could see one run twice and miss another. The instance
id is now the second sort key, which every document store and the @Local routing merge honour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Breaking ties with instance_id ascending under created_at_utc descending matched no index, so Mongo sorted every
run of the task in memory: on 2,000 runs of ~150 KB, a page past the first sorted 301 MB and failed with "Sort
exceeded memory limit" on a server that can't spill to disk. Both keys now descend, and the compound indexes end
in instance_id ((task_id, created_at_utc, instance_id) and the task_version one), so Mongo reads the order from the
index scanned backward with no sort stage; their leading fields still serve a sort on created_at_utc alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit bc6097a into main Oct 8, 2026
14 checks passed
@earakely-scale
earakely-scale deleted the feat/task-instances-read-api branch October 8, 2026 20:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant