Skip to content

feat(py): handle store for tool results - #306

Merged
jat255 merged 2 commits into
jat255/gege-citation-requestfrom
jat255/sevt-handle-store
Sep 7, 2026
Merged

feat(py): handle store for tool results#306
jat255 merged 2 commits into
jat255/gege-citation-requestfrom
jat255/sevt-handle-store

Conversation

@jat255

@jat255 jat255 commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Tool results now get a name (r1, r2, ...) that a later run_python call can reach, so the model can build on an earlier result instead of recomputing it.

Summary

HandleStore.register() stores a result and returns the note the model reads: which handle it got, whether the frame was truncated at 10,000 rows, and what the frame contains. Values that are not frames are stored too, so a scalar measure result stays available for further derivation, and a tool that produced nothing takes no handle.

The frame description follows ellmer::df_schema(), which is what the R agent sees: shape, then per column the dtype and missing count, with ranges for numeric and temporal columns, True and False counts for booleans, and the unique values for the rest when there are few enough and short enough to be worth printing. It caps at 50 columns for the reason ellmer does, so a wide frame cannot flood the prompt.

That description lives in a new _frames.py, which reads pandas and polars frames through what they offer rather than by importing either, since both stay optional. It also takes over the frame predicate _data_source.py had, now that two modules need it.

Review notes

Nothing calls this yet, and that is the shape of the milestone rather than an oversight: the agent class and the tool bodies are blocked behind the layer classes, and run_python belongs to the next milestone. R shows where the calls will go: register_handle() at the measure, pool, and run_sql sites, and handle_ids() with get_handle() feeding the worker namespace.

tests/shared/handles.json pins only what both packages must agree on: the id sequence, that a non-frame value gets one, and the row cap with the sentence that states it. The rest of the note cannot be shared, because the tool that reaches a handle is run_r in R and run_python here, and R's column description comes from ellmer while this one is ours.

Two places where this deliberately reads better than R rather than matching it: the note says "1 row and 1 column" rather than ellmer's "1 rows and 1 columns", and a column with nothing left to take a range over reports only how much is missing instead of a range of nulls.

Testing

Python 991 pass, ruff and pyrefly clean. R 7063 pass, 0 fail. The frame description is tested against real pandas and polars frames, not mocks. I checked the shared fixture can fail rather than trivially pass, by perturbing the expected values in each package's copy.

R-side summary

No R code changes. The R package gains one test file, test-handles.R, which drives new_handle_store(), register_handle(), handle_ids(), and get_handle() from the shared fixture. Those functions had no direct tests before; they were covered only through the tool tests that happen to register a handle, so this adds coverage rather than replacing any.

The test passes max_rows explicitly, so the truncation case runs on a five-row frame instead of building ten thousand rows. Nothing else in the R package is touched, and the generated fixture copy under tests/testthat/fixtures/shared/ comes from scripts/sync-shared.sh.

@jat255 jat255 closed this Sep 7, 2026
@jat255
jat255 deleted the jat255/sevt-handle-store branch September 7, 2026 04:27
@jat255
jat255 restored the jat255/sevt-handle-store branch September 7, 2026 04:29
@jat255 jat255 reopened this Sep 7, 2026
@jat255
jat255 deleted the branch jat255/gege-citation-request September 7, 2026 04:29
@jat255 jat255 closed this Sep 7, 2026
@jat255 jat255 reopened this Sep 7, 2026
@jat255
jat255 changed the base branch from jat255/m5-gege-citation-request to jat255/gege-citation-request September 7, 2026 04:30
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Preview deployed to Connect (dogfood.team.pct.posit.it): https://dogfood.team.pct.posit.it/connect/#/apps/d7a36cae-8f27-448b-a478-61b81fbe3942/draft/368621

Deployed from commit 37681f2.

@jat255 jat255 added this to the py-M5: agent, tools, and prompt milestone Sep 7, 2026
@jat255 jat255 added the py Affects the Python implementation label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Preview deployed to Connect (connect.staging.pct.posit.it): https://connect.staging.pct.posit.it/connect/#/apps/ad662e1b-5048-4acc-9ad7-f9478c92274e/draft/2680

Deployed from commit 37681f2.

@jat255 jat255 added r Affects the R implementation needs-manual-review Agent-created work that needs a human review labels Sep 7, 2026
@jat255
jat255 requested a review from simonpcouch September 7, 2026 05:17
@jat255
jat255 marked this pull request as ready for review September 7, 2026 17:12
@jat255
jat255 force-pushed the jat255/sevt-handle-store branch from 890b79c to e0c6c57 Compare September 7, 2026 17:12
@jat255 jat255 removed the needs-manual-review Agent-created work that needs a human review label Sep 7, 2026
@jat255
jat255 force-pushed the jat255/sevt-handle-store branch from e0c6c57 to 84516a7 Compare September 7, 2026 19:42
A tool result is stored under `r1`, `r2`, ... so a later `run_python` call
can build on it as a plain variable rather than repeating the work.
`HandleStore.register()` returns the note the model gets back, caps a stored
frame at 10,000 rows and says so, and registers values that are not frames
too, so a scalar measure result stays available for further derivation.

The note describes a frame the way `ellmer::df_schema()` does for the R
agent: shape, then per column the dtype and missing count, with ranges for
numeric and temporal columns, True and False counts for booleans, and unique
values for the rest when there are few enough to be worth printing. That
description lives in `_frames.py`, which reads pandas and polars frames
through what they offer rather than by importing either, and now owns the
frame predicate `_data_source.py` had.

`tests/shared/handles.json` pins what both packages must agree on: the id
sequence, that a value which is not a frame gets one, and the row cap with
the sentence that states it. The rest of the note is each package's own,
since the tool that reaches a handle is `run_r` in R and `run_python` here.
Review findings on the handle store:

- describe_frame() reads columns by position, so a pandas frame with
  duplicate column names describes instead of crashing register().
- A column of unhashable values (lists, arrays) omits its unique count
  rather than failing the whole description, and unique values are
  quoted with json.dumps() so embedded quotes and newlines escape.
- is_frame() also requires len() and [], so a value that merely has
  columns (a database table, say) is no longer mistaken for a frame;
  register() still stores a frame-like value it cannot read, just
  without a description, so a strange result is never lost.
- The store's dataclass field stays out of repr and ==, so logging a
  store does not dump frames and comparing stores does not raise.
- The shared fixture pins the note's opening sentence as a template
  (only the tool name differs by package) and gains a case at exactly
  the row cap; both suites assert the note's whole first line.
- Tests now cover the default 10,000-row cap, the unique-value caps,
  empty frames, all-True booleans, and the 50-column boundary; R gains
  the missing-store and empty-store cases.
@jat255
jat255 force-pushed the jat255/sevt-handle-store branch from 84516a7 to 37681f2 Compare September 7, 2026 19:59
@jat255
jat255 merged commit c5925ba into main Sep 7, 2026
13 checks passed
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Cleaned up 5 preview bundle(s) on https://dogfood.team.pct.posit.it: 368503, 368589, 368595, 368616, 368621

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Cleaned up 5 preview bundle(s) on https://connect.staging.pct.posit.it: 2663, 2666, 2668, 2674, 2680

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

Labels

py Affects the Python implementation r Affects the R implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants