Repository navigation
Name the record types a request names to Stage-1 routing (closes #1980) - #3493
Conversation
Pragmatic Code Review: PR #3493 (issue #1980)Review Type: Initial Review (no prior reviews or comments on the PR; one commit, SummaryThe change is small, well-bounded and follows the pattern the skill-name line already set. I found no defect in the code. The findings are about how far the measurement supports the claims made from it, and about three doc comments that say more than the code does. I checked the PR's central premise against the code and it holds: in Not run: the live model test (as instructed) and the unit tests. Everything below is from reading the code, the diff and the docs. Requirements Check
Code Review Findings🟡 Important1. The measurement covers type requests and vague controls, but not requests that have nothing to do with types
2. The live test's assertions would pass on a regression in both arms
3. "No measurable latency" is not what the timing shows
🟢 Suggestions4. "Exactly the types the prompt describes" overstates the link
5. The cap can drop a type the user named outright
6. Names are kept on one line, but can still read as an instruction
7. ADR-064's "types" section describes a prompt that no longer exists
Nitpicks
What is good
RecommendationAPPROVE The code is correct and a net improvement. Findings 1 to 3 are worth doing before merge: they decide whether the numbers written into ADR-038 can be relied on, and finding 1 is the check that caught the previous Stage-1 change. |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Review addressed (commit
|
| What the prompt listed | Decisions on 17 unrelated requests | Queries reworded with a type name |
|---|---|---|
| every built-in type and five unrelated retrieved types | unchanged | "find what I wrote about rate limiting last year" became "… in Feature Writeup"; Research & Search fell out of the top three |
| every built-in type, retrieved types only when named | unchanged | 7 of 17, e.g. "log production incidents as a task or collection record"; of 11 reworded queries, 2 lost the skill they needed and 2 gained it |
| only the types the message names (shipped) | prompt identical to the one without the line | none |
So Stage 1 is now told of a type, built-in or retrieved, only when the message names it (whole word, singular or plural; "people" names person). A message naming no type gets the prompt it had before this PR. This departs from the issue's wording ("the prompt includes built-in type names"): they are included when the message uses them, which is the only case where they can resolve an ambiguity.
Measurement with the shipped rule
Locked model, three reps per case, every rep agreeing. 12 of 40 cases name a type and were run; the other 28 send an identical prompt in both arms.
- Requests naming a known type: 4 of 7 routed without the line, 7 of 7 with it. "how many people do we have?", "what projects are there?" and "tell me about the vendors" moved from clarify to route.
- Type-naming requests never in doubt (5, including the routing eval's task and compound prompts): same decision in both arms, same query but for a dropped word.
- Prompt: 792 → 804 tokens on a turn that has the line. Generation: 2419 ms → 2440 ms over the 27 runs where both arms decided alike.
live_stage1_compound_intentwith the line: 11 of 11route_multi, 10 faithful and 1 that dropped an intent. I have no without-line run of that harness to compare against.
By finding
| # | Finding | Outcome |
|---|---|---|
| 1 | Measurement misses unrelated requests and long type lines | Addressed. The routing eval's single-turn prompts are cases in the live test; the result is the redesign above. A long line of unrelated names can no longer occur. I did not run the daemon-based routing eval: for the 13 of its prompts that name no type, the Stage-1 prompt is byte-identical to main's. |
| 2 | Assertions only relative | Addressed. The no-subject cases are asserted to leave the prompt unchanged (also a unit test, a_message_naming_no_type_gets_no_type_line); the moved cases are asserted to route on every rep. |
| 3 | Latency claim unsupported | Addressed. Arms alternate order; latency is compared only over runs where both arms made the same decision. |
| 4 | "Exactly the types the prompt describes" | Addressed. Comments reworded. |
| 5 | Cap can drop a named type | Moot. Only named types are listed, and the cap (8) applies to them. |
| 6 | A name can still read as an instruction | Skipped, as the review recommends. Exposure is narrower now: a name reaches the prompt only if the user's own message contains its words. |
| 7 | ADR-064's "types" section may be stale | No change needed. "What types do we have?" names no type, so its prompt is unchanged and the section still describes it. |
| nit | STAGE1_SKILL_NAME_MAX_CHARS |
Renamed to STAGE1_NAME_MAX_CHARS. |
| nit | is_empty() guard in the daemon |
Left; it mirrors the guard above it. |
| nit | "matched to this turn's message" | Comments corrected to include recently created schemas. |
Accepted cost
A request that refers to a type without naming it ("do we have any incidents?" for an Incident Report type) gets no line and behaves as it did before.
Checks
bun run test:changed passes (Rust and scripts tiers); bun run quality:fix clean.
Re-review: needed (the type-selection rule and its tests are new code).
Pragmatic Code Review: PR #3493 (issue #1980)Review Type: Re-Review (of commit SummaryThe second commit replaces "list every built-in type and every retrieved type" with "list only the types the message names". The rule is implemented correctly, it is simpler to reason about than the first design, and a message that names no type now provably gets the prompt it has on main. I found no defect in the code. What I found is in three places: two written claims that say more than the code does, one gap between the rule's matcher and the set it is applied to, and tests that do not yet guard the failure the redesign was built around (a type name leaking into Stage 1's query). How this was checked:
Previous Review Summary
Notes on the ones I checked independently:
Requirements Check
Code Review Findings🟡 Important1. Two written claims do not match the case table
2. The rule's matcher is looser than the matcher that builds its candidate set
3. "Singular or plural" covers
🟢 Suggestions4. The tests do not guard the failure the redesign exists to prevent
5. The evidence for the rejected designs is not reproducible from the repository
Nitpicks
Security and performance
What is good
RecommendationAPPROVE The code is correct and a net improvement over both main and the first commit. Finding 1 should be corrected before the docs are committed, since it is a factual error in an ADR table. Findings 2 and 3 are either a small code change or one sentence each in the ADR's accepted cost. Finding 4 is worth doing in this PR if another live run is planned anyway. |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
89886ee to
7582fa5
Compare
Re-review addressed (commit
|
| # | Finding | Outcome |
|---|---|---|
| 1 | Two written claims don't match the case table | Fixed. The ADR table's third row now says 14 of the 17 get the prompt without the line and the 3 that name task get it and decide the same. The case counts are gone with the restructured test; the PR body is rewritten from the final run. |
| 2 | The rule's matcher is looser than the one that builds its candidates | Fixed by the second option. Stage 1 reads every non-core schema's display name through a new AgentToolExecutor::user_type_names (the pattern skill_names uses) and one matcher, user_message_names_type, decides. The session field, its setter, and the daemon and core plumbing are removed: those two files are back to main's. Stage 1 no longer depends on what semantic retrieval ranked. |
| 3 | "Singular or plural" covers -s and -es only |
Fixed. words_match_modulo_plural pairs a consonant + y with ies, unit-tested through stage1_type_names ("companies" names Company). Irregular plurals stay unmatched and the ADR says so. |
| 4 | Tests don't guard the failure the redesign prevents | Fixed. The live test fails when an unrelated case's query gains a type word its message lacks (type_words_added), and asserts unrelated cases absolutely. Three cases use a type's name as an ordinary word. The no-subject group moved to the gated unit test, which now covers all twelve plus the two accepted misses. The test is renamed to what it runs. Reps are 4, so each arm goes first equally. |
| 5 | Evidence for the rejected designs isn't reproducible | Skipped. Keeping a hand-built always-listed arm means maintaining a prompt that ships nowhere; the ADR records the two rejected arms and says they can't be re-run from the repository. |
| nit | "the first is its name in the prompt" | Comment corrected: the prompt names a built-in type by its type id. |
| nit | Group::KnownType describes cases that name no type |
Those three cases are out of the live table; two are in the unit test as accepted misses. |
| nit | Odd rep count | Now 4. |
| nit | Single-character words dropped from a name | Left. It is the existing matcher's rule and costs one extra or one missing name. |
| nit | Message tokenised once per candidate | Left; negligible next to the model call. |
Final measurement
Same model, four reps per case, every rep of a case deciding alike:
- Requests about records of a known type: 4 of 7 routed without the line, 7 of 7 with it.
- Type-naming requests never in doubt: 8 of 8 in both arms, with identical queries; no type word added to any.
- Prompt 792 → 803 tokens. Generation 2313 ms → 2397 ms over 36 runs where both arms decided alike, 23 tokens generated in each.
- "what's the date today?" (no expectation): clarified without the line; with it Stage 1 called no routing tool, which the loop treats as a query on the raw message.
One correction to my earlier comment: the latency comparison there was skewed. Each case's first run without the line follows the previous case's run without the line, so it reuses an already-evaluated system prompt and takes about a quarter of the time (roughly 540 ms against 2100 ms), in that arm only. The comparison now leaves out each case's first rep.
Checks
bun run test:changed passes on the rebased branch; bun run quality:fix clean.
Re-review: needed (user_type_names and the plural rule are new code).
Pragmatic Code Review: PR #3493 (issue #1980)Review Type: Re-Review (third round; end state of SummaryThe branch is one commit and touches only I found no defect that blocks the merge. There is one finding I rate Important: the new read skips the governance participation rule, so an archived type is named to Stage 1. The rest are suggestions about what the live test can and cannot detect. How this was checked:
Previous Review Summary
Checked independently:
Requirements Check
The PR body, the live test's doc comment, Code Review Findings🟡 Important1. An archived type is named to Stage 1
🟢 Suggestions2. The live test asserts four of the seven known-type cases, and its query guard cannot be tripped by the line it guards
3. The latency sample is not order-balanced, in the direction that overstates the cost
4. The plural rule has one positive test and no negative one
Nitpicks
What changed since the last review, on its merits
The plural rule (
Security
Project rules
Docs
What is good
RecommendationAPPROVE The change is correct and a net improvement. Finding 1 is a one-line filter and one test row, worth folding in before merge. Suggestion 2's first part (assert all seven) is a small edit to an ignored test and needs no new model run to be valid. The rest is optional. |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review 2): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Re-review 2 addressed (commit
|
| # | Finding | Outcome |
|---|---|---|
| 1 | An archived type is named to Stage 1 | Fixed. user_type_names keeps a schema only if governance::participates says it takes part; the unit test archives a third schema and expects it left out. The two older readers of the unfiltered corpus on main are outside this PR. |
| 2 | Live test asserts four of seven known-type cases; its query guard can't be tripped by the line | Fixed. Every known-type case is asserted to route on every rep. Each never-in-doubt case is asserted to get the same query in both arms, which is what catches a type's name being re-read as a record type. type_words_added stays as the guard against listing a type the message doesn't name. Its singular still strips only a final "s"; left, since the equal-query assertion now covers those cases. |
| 3 | Latency sample not order-balanced | Comment corrected to say so: over the three measured reps the arm with the line goes first twice, which counts against it. The documented "under 4%" stands as an upper bound. |
| 4 | Plural rule has one positive test and no negative | Fixed. "categories" names Category; "daies" does not name Day. |
| nit | Comment describing the removed retrieval design | Fixed. |
| nit | "every routed turn carries the whole list" | Fixed. |
| nit | get_all_schemas loads every row and edge for names |
Left; negligible next to the model call. |
| nit | Order of the user's names is the store's | Comment now says so. |
The live test passes on the locked model with the stricter assertions: 7 of 7 known-type cases routed, 8 of 8 unrelated cases decided as expected with unchanged queries, 792 → 803 prompt tokens, 2321 ms → 2409 ms over 36 runs. bun run test:changed passes.
Re-Review Decision
Decision: NO RE-REVIEW NEEDED
Rationale: the only product-code change is a one-clause filter using the existing governance predicate, covered by a unit test. The rest is test assertions and comments.
|
Merge queue: #3493 was taken out of the queue — the merge gate failed on it, rebased onto main. Reproduce with |
b5f8288 to
dcce947
Compare
Stage 1 saw the skills but nothing about the data, so a request built on a
known type read as plain English and was clarified. Its prompt now names
each record type the message names: the built-in ones, from the core type
registry, and the user's own, read from the stored schemas through the tool
executor the way skill names are.
Only a type the message names is listed. Listing every type left Stage 1's
decision alone but reworded the query it writes ("log production incidents
as a task or collection record"), and retrieval embeds that query. A message
that names no type gets the prompt it had before.
- words_match_modulo_plural pairs a consonant + "y" with "ies"
- "people" names person
Measured on the locked model: of seven requests about records of a known
type, four routed without the line and seven with it; eight type-naming
requests that were never in doubt decided the same with the same queries.
11 prompt tokens, under 4% more generation time.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
user_type_names read every stored schema, so a type the user archived was still named to Stage 1. It now asks governance whether the schema takes part, as skill_names does through its query. - Live test asserts all seven known-type cases, and that a request never in doubt gets the same query with its type named as without - Plural rule: a positive and a negative case on the matcher itself - Comments corrected where they described the removed session plumbing Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main added the subtype while this was queued; the exhaustive match requires every core type to be placed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dcce947 to
95c39e1
Compare
Closes #1980
What changed
Stage 1's prompt now names the record types the user's message names, next to the skills it already names:
text,date,task,project,person,collection,query, chosen from the core type registry through an exhaustive match, so a new core type has to be placed on one side.AgentToolExecutor::user_type_names, the way skill names are read.A type from either list reaches the prompt only when the message names it (whole word, singular or regular plural; "people" names
person), at most eight. A message naming no type gets the same prompt as before.What differs from the issue
Measurement
Locked model (Gemma 4 E4B), the type line the only difference between arms, four reps per case, every rep of a case deciding alike.
Accepted costs:
date. Without the line it clarified; with it Stage 1 called no routing tool, which the loop treats as a query on the raw message.The measurement is
live_stage1_golden_prompts::stage1_type_line_routes_requests_that_name_a_known_type(ignored by default; needs the GGUF).Acceptance criteria
route_clarifystill fires for genuinely vague requests (their prompt is unchanged)bun run test:changedpasses (Rust and scripts tiers)bun run quality:fixclean🤖 Generated with Claude Code