Repository navigation
Hold a routed turn's type arguments to the schemas its skills link to (closes #3419) - #3497
Conversation
Pragmatic Code Review: PR #3497 (issue #3419)Review Type: Initial Review (no prior reviews or comments on the PR) SummaryThe Rust change is correct, small for what it does, and consistent with the surrounding code. I found no defect in the enforcement path. The open points are about what the eval can prove, what the model is told on a held turn, and two acceptance criteria that are still outstanding. How I checked: I read the diff and the surrounding code in What holds up:
Both deliberate differences from the issue text are right:
Requirements Check
Code Review Findings🟡 1. The eval cannot demonstrate criterion 6 as the fixture stands📁 There are two separate problems.
Recommendation, before the eval run:
Principle: a measurement has to be able to fail to count as evidence. 🟡 2. On a held turn the parameter description contradicts its own
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: COMMENT. No blocking code defects; acceptance criteria 6 (eval) and 8 (docs commit) are still open, and findings 1-3 need a change or an explicit decision before merge. Full findings in the review comment above.
Review addressed (commit 22903e2)All eight findings were acted on. Nothing was skipped. 1. The eval could not demonstrate the change — fixed in the fixture.
2. Description contradicted the 3. The type the user named was excluded — took the first option. The set is now every type the Stage-2 candidate block lists (the 4. 5. 6. 7. Test gaps — added: the fail-open surface is not held, a refused call re-sent with an offered type runs, and one round refuses only its off-menu call. The fail-open gate and the precise 8. Minor points — Also fixed, found while running the fixture: the decision fixture read Still open: the full Re-Review DecisionDecision: RE-REVIEW NEEDED Rationale: finding 3 changed what the offered set contains, finding 2 changed model-facing text, and the fixture gained scenarios and scoring. Those are design changes, not mechanical ones. |
Pragmatic Code Review: PR #3497 (issue #3419)Review Type: Re-Review (covers How I checked: I read the follow-up diff and the surrounding code in Previous Review Summary
Requirements Check
Code Review Findings🟡 A. A core type the user names is still refused, and the new scenario scores a wrong-type write as a pass📁
Recommendation:
Principle: a passing verdict should not be reachable through the failure it sits next to. 🟡 B. The PR body describes the first commit, not the code📁 PR #3497 description These statements are no longer true:
Recommendation: rewrite the body before merge. It is the record of what the change does. 🟢 C. The control scenario passes on a turn that was not held📁
Recommendation: fail it as unmeasured when no schema decision is 🟢 D. "An off-menu call ran on a held turn" can miss a call that ran📁 It can fail in the simple case (one off-menu selection, no refusal), and the test pins that. Two gaps remain:
A separate fragility in the marker: 🟢 E.
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): COMMENT. All 8 previous findings addressed; no code defect in the follow-up commit. Open before merge: a decision on core types named on a held turn (finding A), the stale PR body (finding B), and acceptance criteria 6 (eval) and 8 (docs commit). Full findings in the review comment above.
Re-review addressed (commit dbf3ee0)A. A named core type is still refused, and the scenario could pass after a wrong-type write
B. Stale PR body — will be rewritten when the eval results are in, so it carries them. C. Control passed on an open turn — a linked-skill scenario now fails as unmeasured when no schema decision on its turn was enforced. D. A refused call could cover a second one that ran — the check compares counts (enforced off-menu decisions against refused calls). The first-call-per-round limit is stated in the comment. The marker no longer depends on the truncated preview: the E. F. Wording — a held description opens with what the parameter is for ( G. Tool-less skill fallback — one sentence in the doc comment and the ADR. H. Seeding — the comment says the isolation holds within a rep and needs Tests: 917 agent tests and 1272 script tests pass; Re-Review DecisionDecision: RE-REVIEW NEEDED Rationale: the refusal's model-facing wording changed, the log line gained a field the scrape depends on, and the fixture's scoring changed in several connected places. |
Pragmatic Code Review: PR #3497 (issue #3419)Review Type: Re-Review (second), covering How I checked: I read the follow-up diff and the committed code around it ( Previous Review Summary
Requirements Check
Code Review Findings🟡 1. The count comparison reports "ran" for calls that did not run📁
The previous Recommendation:
Principle: a measurement should observe the event it reports, not infer it from two records that can disagree for other reasons. 🟡 2. A wrong-type write with no refusal still passes the off-menu scenario📁 The scenario's doc comment now says the record "must not then be written as an offered type". That is checked only after a refusal ( This is the outcome the Two smaller points in the same function:
Recommendation: for Principle: a passing verdict should not be reachable through the failure the scenario is named for. 🟡 3. The refusal now ties "leave
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (second re-review): COMMENT. No defect in the production path. Three important findings on eval scoring and refusal wording (a false "ran" count, a wrong-type write that passes with no refusal, the search_nodes omit recovery tied to the wrong condition). Criteria 6 (eval) and 8 (docs commit) and the finding A decision are still open. Full findings in the review comment above.
Second re-review addressed (commit b1aeb7c)1. The count comparison reported "ran" for calls that did not run — replaced with a direct observation. The 2. A wrong-type write with no refusal still passed — 3. The refusal tied "leave node_type out" to the wrong condition — for an optional type the message now reads "If none of them is, leave node_type out to search every type." For a required type it keeps "do not use one anyway" and then 4. 6. No change needed, and the field order is now confirmed against a real log line: 7. Nits — Tests: 918 agent tests and 1275 script tests pass; Re-Review DecisionDecision: RE-REVIEW NEEDED Rationale: a new log field and the flag behind it were added to the dispatch path, the refusal's wording changed again, and the scenario's pass condition changed. |
Pragmatic Code Review: PR #3497 (issue #3419)Review Type: Re-Review (third), covering How I checked: I read the follow-up diff and the committed code around it ( Previous Review Summary
Requirements Check
What I verified in this commit
Refusal message: the three compositions read correctly, and the test would fail on any wording change. Scrape and parser: Fixture scoring
PR body and docs: both match the code, including Code Review FindingsNo critical or important findings. Three suggestions, none of which changes a verdict or the headline numbers. 🟢 1.
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (third re-review): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Third re-review addressed (commit cf28c4e)The third re-review approved with three suggestions. 1. "After a refusal" and the wrong-type-record figure counted different things — fixed. 2. 3. Failure label when the create came before the refusal — now "not after a refusal". Tests: 1275 script tests pass; Re-Review DecisionDecision: NO RE-REVIEW NEEDED Rationale: the only changes since the approved commit are a label and a counting rule in the eval fixture's reporting, with their tests. No production code, no model-facing text and no verdict logic changed. Open before this closes: the |
|
Merge queue: #3497 was taken out of the queue — it conflicts with main in: packages/agent/src/local_agent/agent_loop.rs. Rebase onto origin/main, push, and re-run |
7411bba to
b75b867
Compare
…closes #3419) A routed turn whose gate-clearing, tool-bearing candidates all carry their linked schemas is held to every type its Stage-2 candidate block lists: the linked schemas and their subtypes, plus the type of a schema-typed hit. A turn with any unlinked tool-bearing candidate, no routing, a fail-open surface or a withheld candidate block is unchanged. - find_skills marks each candidate's schema_metadata as linked or fallback (schemas_linked), since the types alone cannot tell the two apart. - The set goes on the existing-type parameter of create_node, search_nodes, resolve_query (node_type) and update_schema (schema_id) as an enum, with a description that agrees with it. update_node names no type and is not held. - Dispatch refuses a call that names a type outside the set and returns the allowed ids. The refusal makes the re-send conditional, and for an optional type says to leave it out. - The schema decision record carries enforced. The Tool executed log line carries type_refused and off_menu_ran. - Decision fixture: two scenarios against skills linked through applies_to, scored from what dispatch reported; and a decoder for schema list's current shape, which the fixture was reading in a shape the CLI no longer emits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b75b867 to
815ea2a
Compare
Closes #3419.
What changes
A routed turn whose matched skills all link to their schemas is now held to the types its candidate block lists.
routing::offered_types): when every candidate that clears its score bar and whitelists a tool carries its linked schemas. One unlinked tool-bearing candidate, an unrouted turn, a fail-open surface or a withheld candidate block (routing_disabled) leaves the turn exactly as it was.schema_metadataof every candidate that clears its bar, deduplicated in presentation order. That is the linked schemas and their subtypes, plus the type of any schema-typed retrieval hit, which is how a custom type the request names outright reaches the block.find_skills→schemas_linked): retrieval marks each candidate's metadata. The types alone cannot tell the two apart: an unlinked skill whose query namesinvoicecarries the same single type a link toinvoicewould.routing::hold_to_offered_types):enum: [<set>]on the existing-type parameter ofcreate_node,search_nodes,resolve_query(node_type) andupdate_schema(schema_id), with the parameter's description replaced by one that agrees with theenum. The list comes fromTool::existing_type_parameter, an exhaustive match.type_not_offered) with the allowed ids. Its message tells the model to re-send only if one of them is what the user meant. If none is, it says to leave an optional type out (which searches every type), or, for a required type, not to use one anyway and to callroute_clarifywhen that is on the turn's surface.enforcedon the record and in the log payload. On a held turn the schema decision's candidates are the offered set, and the record is enforced when its selection came from a call dispatch holds. TheTool executedlog line carriestype_refusedandoff_menu_ran(a call naming an off-menu type reached the executor, which the refusal makes false).Where this differs from the issue text
update_nodegets noenumand is never refused. The issue lists it, but it takes a node id and has no type parameter.update_schemanames its type inschema_id, and that is the parameter held.selected_schemareadsschema_idforupdate_schema. It read onlynode_type/type, so anupdate_schemaselection was never recorded. Schema-decision counts forupdate_schematurns shift for that reason alone.Consequences a reviewer should weigh
search_nodesandresolve_queryare held too, and a relationship's target type is not part of a skill's linked set.search_nodeswithnode_type: "schema"is refused on a held turn. Leavingnode_typeout still searches every type.declare_write_tool_fields), so the per-turnenumadds no new divergence point.Eval fixture
applies_to(held-on-menu-type,held-off-menu-type). Their groups seed awarranty_claimtype and three skills linked to it. They sit last so the extra skills do not change what the other scenarios are scored against within a rep.type_refused,off_menu_ran), not inferred from the decision records. The off-menu scenario fails if a record is created on the held turn at all, refused first or not, since any record created there is of an offered type. What followed a refusal is recorded: asked the user, replied in prose, or wrote a record.schema listin a shape the CLI no longer emits (nodes[].properties.isCore), so on a warm database it found no schema. One decoder with a test now readsschemas[]with flatis_coreandfields.Goldens
stage2_tool_surface_offered_types.golden(new) holds each parameter that changes on a held turn, in full.decision-wire-format.goldengainsenforcedon every line and a tenth line for a refused selection.Tests
routing: when a set exists, the set equalling the ids the candidate block renders, tool-less and below-bar candidates, theenumon exactly the tools that name an existing type.tools: which tools name an existing type and that each declares that parameter, the held descriptions,off_menu_typeper tool.agent_loop: theenumon the Stage-2 surface, refusal per tool with the allowed ids, a re-sent call running, a mixed round, the refusal's wording, the schema hit's type being offered, unlinked / mixed / routing-disabled / fail-open turns unchanged, the enforced decision log line.find_skillsintegration:schemas_linkedfor linked and fallback metadata.enum, the refusal, the fail-open gate or the preciseenforcedflag fails a test, and enforcing on unlinked or disabled turns fails a test.bun run test:changedpasses.bun run quality:fixis clean.Eval results
eval:decisionson the locked model, one rep each, before (offered set switched off) and after. Full write-up: #3419 (comment)tasktwice and both calls reached the executor. After, the turn was held, no call named an off-menu type, no record was created, and the model asked the user.create_nodeon the linked type ran with its fields.task). It did fire in an earlier trial of the same scenario on this branch.The ADR-038 amendment is committed in nodespace-docs.
🤖 Generated with Claude Code