fix(agent): blocked stale labels in last-turn yields
- Lifted the unknown-incremental-label check above the !useLastTurn guard so type: [findings], result: {} also rejects, preventing a sibling section's MAX_SCHEMA_RETRIES override from sneaking the stale section through finalization.\n- Extended the regression test with the last-turn payload case.\n\nRefs #3926
This commit is contained in:
@@ -321,26 +321,27 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
||||
|
||||
const status = errorMessage !== undefined ? "aborted" : "success";
|
||||
let schemaValidationOverridden = false;
|
||||
// Unknown incremental labels are a hard contract mismatch with the closed caller
|
||||
// schema. Reject before the last-turn short-circuit too: `type: ["findings"], result: {}`
|
||||
// would otherwise be accepted as a typed last-turn incremental yield, then a sibling
|
||||
// section's MAX_SCHEMA_RETRIES override flips schemaOverridden in finalization and the
|
||||
// stale section rides along untouched.
|
||||
if (status === "success" && isIncremental) {
|
||||
const unknownLabels = this.#unknownIncrementalLabels(yieldType as string[]);
|
||||
if (unknownLabels.length > 0) {
|
||||
const validLabels =
|
||||
this.#validateSection && this.#validateSection.size > 0
|
||||
? formatYieldLabels([...this.#validateSection.keys()])
|
||||
: "none";
|
||||
throw new Error(
|
||||
`Section ${formatYieldLabels(yieldType as string[])} uses unknown incremental yield label(s): ${formatYieldLabels(unknownLabels)}. Resubmit with one of the schema's labels: ${validLabels}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (status === "success" && !useLastTurn) {
|
||||
if (data === null) {
|
||||
throw new Error("data is required when yield indicates success");
|
||||
}
|
||||
// Unknown incremental labels are a hard contract mismatch with the closed
|
||||
// caller schema, not a shape error a retry can fix. Reject them every time
|
||||
// without touching the schema-retry counter so the override path never
|
||||
// accepts a stale agent-native section name.
|
||||
if (isIncremental) {
|
||||
const unknownLabels = this.#unknownIncrementalLabels(yieldType as string[]);
|
||||
if (unknownLabels.length > 0) {
|
||||
const validLabels =
|
||||
this.#validateSection && this.#validateSection.size > 0
|
||||
? formatYieldLabels([...this.#validateSection.keys()])
|
||||
: "none";
|
||||
throw new Error(
|
||||
`Section ${formatYieldLabels(yieldType as string[])} uses unknown incremental yield label(s): ${formatYieldLabels(unknownLabels)}. Resubmit with one of the schema's labels: ${validLabels}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const sectionFailure = isIncremental
|
||||
? this.#validateIncrementalSection(yieldType as string[], data)
|
||||
: this.#validate
|
||||
|
||||
@@ -282,6 +282,19 @@ describe("YieldTool", () => {
|
||||
);
|
||||
}
|
||||
|
||||
// The last-turn short-circuit (`type: ["findings"], result: {}`) MUST also reject
|
||||
// the unknown label. Otherwise the stale section silently accepts the last assistant
|
||||
// text and rides along when a sibling section trips MAX_SCHEMA_RETRIES and
|
||||
// schemaOverridden in finalization (issue #3927 follow-up review).
|
||||
await expect(
|
||||
tool.execute("call-native-reviewer-label-last-turn", {
|
||||
type: ["findings"],
|
||||
result: {},
|
||||
} as never),
|
||||
).rejects.toThrow(
|
||||
/Section "findings" uses unknown incremental yield label\(s\): "findings"\. Resubmit with one of the schema's labels: "issue_key", "verdict", "blockers", "non_blocking_notes"\./,
|
||||
);
|
||||
|
||||
// Schema-retry budget intact: a separate, shape-only mismatch still fires the
|
||||
// first-attempt retry hint (`2 retry attempt(s) remain`), proving the unknown-label
|
||||
// path didn't burn the override.
|
||||
|
||||
Reference in New Issue
Block a user