Skip to content

fix(agent): month-year dates, the minus sign, and checks for open roles - #12512

Merged
MarkusNeusinger merged 14 commits into
mainfrom
fix/agent-month-year-dates
Nov 2, 2026
Merged

MarkusNeusinger merged 14 commits into
mainfrom
fix/agent-month-year-dates

Conversation

@MarkusNeusinger

@MarkusNeusinger MarkusNeusinger commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

The owner's live tests of 2026-11-01 and 11-02: a pasted RTX 4090 price table (Nov 2026 to Dec 2022, newest first, a change column of +75 and −100, a website's button line between the rows) left line-timeseries-rolling's date role open and lost its last row, and a pasted 1,3,5,4,6,2 left a line plot without an x. The root asked the user. Now both reach a shipped plot without a word about dates or x.

  • Month and year in one cell are dates. months.month_year reads Nov 2026, November 2026, Okt. 2026, 2026 Nov, März 2026, 2026年3月 and tháng 3 năm 2026 (the eleven languages of months.py). The parser reads such a column as the first day of each month, with a note that counts the cells, like the YYYY-MM path. set_type to datetime reads the same cells. It also no longer accepts the dates pandas only fills in: a cell without a digit (May, now), a time alone (today's date) or a missing year (the year 1).
  • The parser goes by row shape. A line between the data rows that fills one cell, text under a value column, is left out with a count (Show 36 earlier months). A last row that lost its empty cells but holds two values (Dec 2022, two empty cells, $2099 under five columns) stays a row. Nothing here is tied to one site.
  • The minus sign is a minus. − (U+2212) before a number is a sign in the parser and in set_type number, with a note that counts the cells. An en dash is no minus: 10–20 and –5 stay text.
  • An open required role triggers a check in code. data/conversions.py dry-runs set_type (prepare.dry_run) on every free text column, and on a whole-number column with a year header, and lists each candidate in the session block. A line holds the counts, certain or uncertain with the reason, and the operations: set_type with the new name field (a new column beside the original) plus a sort when the dates run newest first. The root makes a certain conversion in the summary turn and says so with the count, and proposes an uncertain one as its one question. The service binds the new column to the role, and takes the source column off the optional roles.
  • Row numbers are the last fallback. The new row_number operation adds a column index of 1 to n. When a required axis has no column and no conversion, the session block lists it as certain, the axis takes it, and the reply says so in one line. It is a closed operation of its own, so a later dropdown entry "index (row number)" can call it.
  • No ordering question. ColumnProfile.order says which way a date column runs. The root sorts a newest-first date column of the plot ascending in the summary turn and names it.
  • Months stay months on a bar chart. ColumnProfile.monthly marks a column of months. The adapter writes it as Nov 2026, never as an ISO date, and it binds a bar chart's category past 60 values, as the same column did when it was text.
  • Measured. The prep eval fails a skipped certain conversion in a summary turn, and chat_turn.conversion reports one closed value: none, date, number, index or asked.

Decisions (decided and documented here)

  • Two-digit years stay text (Nov 26, 26.11.26). Nov 26 is as often 26 November, and the century is a guess, the same reason make_date refuses a year 24. The parser keeps them text. In the dry run, 26.11.26 reads only with a century guess, so it is uncertain and the root asks.
  • What counts as uncertain: some cells would be missing (at most 10 %; more is no candidate, as set_type refuses it), or the reading guesses one of the following. The day and month order (only cells without a month name count), the century of a two-digit year, the decimal mark, spaces or apostrophes as thousands separators, leading zeros (007), or digits a number cannot hold.
  • What is no candidate at all: a column whose name or frequent cells say ids or codes, a column named as a span (Season), YYYY-NN cells the parser kept text (2009-10, _as_periods), and a range (10–20).
  • A year needs a year header and one row per year. Prices of 1149 to 3000 look like years. Year, Month, Temperature would put every month on 1 January, and make_date builds those dates.
  • A certain conversion is automatic in the summary turn only. In a later turn a line matters only when the message is about the plot or the open role, and then the root asks, so an undone conversion never comes back unasked.
  • A left-out line is no note. The first live run kept the button's text in the notes, the root took it for a wish, and the plot showed 36 months. The words of a page are not the user's, so only the count and the line number stay.
  • A sparse row stays. The stray-line rule needs one filled cell, text under a value column. So Ben under Name,Score, a full row Dora,pending and a short row Bob,A,absent stay rows, and a heading such as Asia between countries stays a row too, as before. A button above a text first column reads the same as Ben, so it stays a row with empty cells: a line only a human could tell apart is not guessed at.
  • A short last row needs two values. With one, a footer such as * See note,2024 would become a plotted row. A row left below the table is said in a warning, and a footer read as data is not, so a short Clara,7 under three columns stays out, as before.
  • A free column that fits goes before row numbers. The row number fills a role only when no column and no conversion can.
  • One number column sits on x, so y is the open role. The defaults fill roles in spec order, which the brief did not expect: on 1,3,5,4,6,2 the transcriber writes one column value, x takes it, and y stays open. bindings.axis_slot covers both shapes: the row number takes the axis, and the number column moves to the measure. Real dates come first: 8 Nov 2026,3000 on a line plot converts the dates for x and moves the numbers to y, never a row number in place of dates. A year or a row number on x stays there.
  • The bar chart case: a question, never automatic. A role that names each row (category, label, name) gets the row numbers as an uncertain line. A number says nothing as a label, and 8 of the 21 specs with such a role and one measure are distributions by group (box, violin, strip, swarm, raincloud), where it would make one group per row. A date role, a measure or a series never takes row numbers. The later dropdown entry will offer it to the user directly.
  • index keeps its name in every language, as the brief names it. A taken name gets index_2, and a row-number column the paste already has is reused, with nothing added. A pasted row number now stays on the axis wherever it stands (weight, X), and day, X keeps its dates on x.
  • /dataset/copy runs none of this. It copies confirmed data byte for byte, and the conversions live only in the session block and the root's summary turn.
  • asked means listed and not made. asked_question on the same line tells a question from a skip.
  • The new column is named after its source with the type in the session language (Month (date), Monat (Datum)), as shape.LONG_NAMES does. The root copies the operations, so the run is reproducible.
  • Dry runs are cached per dataset version on StoredDataset.dry_runs, a non-init field, so a copy() starts empty. Each column first reads its first 20 distinct cells, so a column of names costs no full read.
  • prepare_data returns the bindings when a conversion moved them. The root otherwise named the ones the turn started from.
  • A dash is no unit. Without the en dash as a minus, set_type would have read –5 as 5 with the unit –, dropping the sign.

Live check (Haiku 5.5, eu, deployed renderer)

All four owner cases shipped on the first or second attempt, with no user word about dates or x. Paste, summary, .confirm() and .create_plot() in process. Cost: $0.10 for these four, $0.34 for all Haiku runs of the additions, against the cap of $1.

The 48-month paste, English on matplotlib, line-timeseries-rolling: 48 rows, Dec 2022 present, no Show row, Month as dates, Δ as integers.

The table has 48 monthly rows from December 2022 to November 2026. Month is bound to the date role and "Used ($)" to the value role, which is the median lowest daily price per month in dollars, as your notes describe. I sorted the rows from oldest to newest, because the plot draws them in that order. […]

The plot is ready: a line chart of the median lowest daily RTX 4090 price per month, in dollars, from 2022-12 to 2026-11. The thin line shows the raw monthly prices, and a bold green line shows their 3-month rolling average […]

Its German variant on seaborn (Okt. 2026, −, prices in €):

Der Datensatz hat 48 monatliche Zeilen von Dezember 2022 bis November 2026 und drei Spalten. Die Zeitachse ist Monat, der Wert ist Gebraucht (€), also die Preise je Monat zwischen 699 € und 3000 €. Ich habe die Zeilen von der neuesten zur ältesten Zeile in aufsteigende Reihenfolge gebracht, damit die Kurve von links nach rechts läuft. […]

Der Plot ist fertig: Eine Linie zeigt den Gebrauchtpreis je Monat, eine kräftige Linie den gleitenden Durchschnitt über 6 Monate […]

1,3,5,4,6,2 on line-basic, matplotlib (the transcriber reads it, .use() posts it):

The table has 6 rows and one integer column, value, with the numbers 1 to 6. Since it has no x column, I added index with the row numbers 1 to 6 and bound it to x, so x is index and y is value. […]

The plot is ready: a line with markers showing value against index (1 to 6), titled "Value by index". […]

The same on ECharts:

The table has 6 rows and one integer column, value, with values from 1 to 6. The spec has no x column, so I added index with the row numbers 1 to 6 and bound it to x; value is bound to y. […]

The plot is ready: a line chart of value against index, titled "Value by index", with one point per row. It has a dashed mean line at 3.5 and a label for the latest value, 2. […]

How often the root applies a certain conversion: 12 of 12 in three runs of the 11 summary cases ($0.056). Exact match 32 of 33. The one miss, de-summary-zeilennummer, made the column, dropped it and made it again: the end state was right, the steps were not. The reply gate held, with 0 reply misses and medians of 58 to 62 words. These three runs came before the last commit, which changes only the parser's row shape and the row-number slot.

Bar-type check on bar-basic: 12 months on matplotlib, before the adapter rule, read 2025-12; on Chart.js, with it, Dec 2025. 70 months on matplotlib bound the category and shipped with labels every eighth bar (Jan 2020, Sep 2020), one bar per month. None showed 2026-11-01. On Chart.js the reply once called the highest month October; the plot was right, and that slip is the root's reading of its own result, not the data.

What the first live pass found, fixed before this one: the root read the button line as a wish and plotted 36 months, and it called the row-number column a mistake. A sweep of the prep fixtures found that Year, Month, Temperature would have owed a conversion of the year alone.

Flash pre-check (Gemini 3.8 Flash)

Ran on the additions only (diff against a9d5228, $0.53). It stopped at its token limit during its fourth finding, so the answer is partial.

  • Applied: a footer with one value became a row (short_row).
  • Applied: a short real row could be dropped as a stray line (stray_row).
  • Applied: row numbers were offered while a free column fit the role (row_slot).
  • Skipped: after an .undo() of an axis move, y keeps the column and x is the open role, not the reverse. The bindings stay valid and the session block still lists the row numbers.
  • Skipped: two conversions in one turn report one outcome. The brief asks for one closed value per turn.

Plan

N/A (brief from the main session)

Test plan

  • uv run pytest tests/unit/agents tests/unit/api/test_agent_router.py -n 4 on the merged tree: 20,208 passed, 36 skipped. New tests cover every point: tests/unit/agents/data/test_months.py (the five forms, and linear time on 200,000 separators), TestLinesThatAreNoRows (48 rows, the true negatives), TestRowNumber, TestRowNumbers, TestAxisAndRowNumbers, TestMonthsAsLabels, TestYears, source unbinding, the eval's skip check, the telemetry outcome, and a service flow from the session block to the bindings and .undo().
  • Three new prep eval summary cases: a list of numbers in English and German, and a Year column for a date role.
  • ruff check ., ruff format --check ., mypy api core agents
  • uv run python -m tools.changelog check --base origin/main

After the merge

  • Re-apply agents/telemetry/views/02_event_views.sql (the recipe is in docs/reference/agent-telemetry.md), so the chat_turns view gets its conversion column. The line itself carries the field from the deploy on.

https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

Copilot AI lite review requested due to automatic review settings November 1, 2026 23:28
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Comment thread agents/anyplot/data/months.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two moderate issues remain unresolved in prepare.py: the month-name check miscounts columns that repeat a month, and a date conversion can run without confirmation on season-like values such as 2009-10.

3 open issues
What changed in this PR

This PR fixes a live-test failure in the agent's data step. A pasted price table with month-year cells (Nov 2026) and typographic minus signs (−100) left a time series' date role open, so the root asked the user for a date column. The PR teaches the parser and set_type to read those cells, adds a server-side dry run that finds text columns able to fill an open required role, and sorts newest-first date columns in the summary turn.

Changes:

  • The parser and set_type read month names with a four-digit year as the first day of the month, and no longer fill in dates pandas would invent (a bare name, a time alone, or a missing year).
  • A typographic minus (−, or an en dash directly before a number) reads as a sign, with a note that counts the cells. A range such as 10–20 stays text.
  • When a required role is open, the server dry-runs set_type on text columns and lists each candidate in the session block as certain or uncertain. Certain conversions run in the summary turn as a new column, which is bound to the role. Date columns also carry an order flag, so a newest-first column is sorted ascending.
File Description
tests/​unit/​agents/​runtime/​test_service_flow.py Adds a service flow test: an open role lists its conversion, the conversion fills it, and undo restores it.
tests/​unit/​agents/​evals/​test_prep.py Updates the prep eval expectations to 59 cases and 8 summary cases, with the new changing and blocked summaries.
tests/​unit/​agents/​data/​test_warning_codes.py Maps the new month-name and typographic-minus notes to closed warning codes.
tests/​unit/​agents/​data/​test_prepare.py Tests month-year conversion, the refusal of filled-in dates, set_type with name, the minus sign, and the dry run.
tests/​unit/​agents/​data/​test_parse.py Tests month-year parsing, the typographic minus, date order, and the owner's RTX price paste.
tests/​unit/​agents/​data/​test_conversions.py New tests for candidate lines, new column names, the dry-run cache, and the candidate limit.
tests/​unit/​agents/​data/​test_bindings.py Tests that a converted number column or a new date column fills an open required role.
docs/​concepts/​agent-network.md Documents the month-year parsing, the minus rule, the conversion candidates, and the summary-turn sort.
changelog.d/​agent-month-year-dates.md Adds the changelog fragment for the new conversions and the fixes.
agents/​README.md Updates the prep eval case counts and describes the new summary cases.
agents/​evals/​fixtures/​prep/​en-summary-rtx-prices.json New eval case: the owner's RTX price table, expecting a sort of the month column.
agents/​evals/​fixtures/​prep/​en-summary-convert-day-dates.json New eval case: day-first English dates that convert for certain, with a sort.
agents/​evals/​fixtures/​prep/​de-summary-preise-monatsnamen.json New eval case: the German twin of the price table, with month names and a minus sign.
agents/​evals/​fixtures/​prep/​de-summary-datum-zweistellig.json New eval case: German two-digit-year dates that are uncertain, expecting no operation and a question.
agents/​anyplot/​tools/​session.py Passes the converted column names into the version push so they bind to the open role.
agents/​anyplot/​schemas.py Adds the order field to ColumnProfile for ascending or descending date columns.
agents/​anyplot/​prompts/​root.md Tells the root to make certain conversions and sorts in the summary turn, and to ask only about uncertain ones.
agents/​anyplot/​dataset_versions.py Passes converted column names through push_version into carry_bindings.
agents/​anyplot/​data/​warning_codes.py Adds closed warning codes for month-name reads and typographic-minus reads.
agents/​anyplot/​data/​store.py Adds a per-version cache for dry runs on the stored dataset.
agents/​anyplot/​data/​prepare.py Adds the month-year and minus handling in set_type, the name option, and the dry_run function.
agents/​anyplot/​data/​parse.py Reads month-year cells and typographic minus signs in the parser, and profiles the date order.
agents/​anyplot/​data/​months.py Adds month_year, which reads a month name and a four-digit year in one cell.
agents/​anyplot/​data/​conversions.py New module that builds the conversion candidates for open required roles.
agents/​anyplot/​data/​bindings.py Binds a converted number column to an open required number role in carry_bindings.
agents/​anyplot/​agent.py Adds the conversion candidate lines to the session block the root reads.

Review effort: Lite

Comment thread agents/anyplot/data/prepare.py Resolved
Comment thread agents/anyplot/data/prepare.py Resolved
Comment thread agents/anyplot/data/parse.py Resolved
@codecov

codecov Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.94118% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
agents/anyplot/data/bindings.py 95.45% 5 Missing ⚠️
agents/anyplot/data/prepare.py 97.87% 5 Missing ⚠️
agents/anyplot/data/conversions.py 98.62% 2 Missing ⚠️
agents/anyplot/data/parse.py 98.82% 1 Missing ⚠️
agents/evals/prep.py 96.87% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

MarkusNeusinger and others added 8 commits November 2, 2026 01:21
…onversion guards

Review findings and owner-ordered additions to #12512.

Parser: a short line between the rows that holds text in the value columns is
left out with a count, and a last row that lost its empty cells stays. The owner's
48-month paste now reads 48 rows with Month as dates. An en dash is no minus any
more, and a dash is no unit, so `–5` keeps its sign or stays text. `month_year`
trims its separators by hand (CodeQL py/polynomial-redos).

Row numbers: a closed `row_number` operation, offered as a certain line when a
required axis has no column and no conversion, which also moves the one number
column from x to y. A category without labels gets the offer as a question.

Conversions: ids, codes and spans stay text, a year needs a year header, dates
take the axis before row numbers do, and a converted column takes its source off
the optional roles. `ColumnProfile.monthly` keeps month labels past 60 categories.

The prep eval fails a skipped certain conversion, and `chat_turn` reports one
closed outcome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…years

The first live run of the additions to #12512 found three things.

A left-out line is no note: the root read the button `Show 36 earlier months`
as the user's wish and cut the plot to 36 months. Only the count and the line
number stay.

The row-number line now says what happens, and `prepare_data` returns the
bindings when a conversion moved them. On `1,3,5,4,6,2` the root made the
column, saw x still on `value` and called the column a mistake.

A year column needs one row per year: `Year, Month, Temperature` would have put
every month on 1 January, and `make_date` builds those dates. In a later turn a
conversion line matters only when the message is about the plot or the open
role. Docs and the changelog fragment follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
A short last row now needs two values under the value columns: with one, a
footer such as `* See note,2024` became a plotted row. A stray line fills exactly
one cell, so a short row such as `Bob,A,absent` stays. A free column the open
role accepts goes before row numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Pins what the PR body says about `.undo()` of an axis move: the column stays on
y, x is the open role, and the session block still lists the row numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
@MarkusNeusinger
MarkusNeusinger merged commit bc6f9ee into main Nov 2, 2026
16 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the fix/agent-month-year-dates branch November 2, 2026 07:55
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.

3 participants