fix(coding-agent): suppress WIP advisor non-blockers

This commit is contained in:
Wolfgang Schoenberger
2026-07-30 15:18:06 -07:00
parent 4df68d6043
commit 26e422a00a
5 changed files with 56 additions and 7 deletions
@@ -180,12 +180,23 @@ export class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails>
* escalation: nit → concern → blocker), so an advisor cannot bypass dedupe
* by retagging the same text at a lower or equal severity. */
#deliveredNoteSeverities = new Map<string, number>();
#inProgressUpdate = false;
constructor(private readonly onAdvice: (note: string, severity?: AdviseDetails["severity"]) => void) {}
/**
* Mark whether the next advisor prompt reviews an in-progress primary turn.
* Non-blockers are withheld until a completed update so partial work does
* not interrupt the primary before it can finish its planned steps.
*/
beginUpdate(inProgress: boolean): void {
this.#inProgressUpdate = inProgress;
}
/** Clear delivered-note memory when the advisor starts a fresh conversation. */
resetDeliveredNotes(): void {
this.#deliveredNoteSeverities.clear();
this.#inProgressUpdate = false;
}
async execute(
@@ -195,6 +206,13 @@ export class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails>
_onUpdate?: AgentToolUpdateCallback<AdviseDetails>,
_context?: AgentToolContext,
): Promise<AgentToolResult<AdviseDetails>> {
if (this.#inProgressUpdate && args.severity !== "blocker") {
return {
content: [{ type: "text", text: "Recorded." }],
details: { note: args.note, severity: args.severity },
useless: true,
};
}
const key = advisorNoteDedupeKey(args.note);
const rank = advisorSeverityRank(args.severity);
const previousRank = this.#deliveredNoteSeverities.get(key) ?? 0;
+6 -6
View File
@@ -51,11 +51,11 @@ export interface AdvisorRuntimeHost {
maintainContext?(incomingTokens: number, signal: AbortSignal): Promise<boolean>;
/**
* Called immediately before each `agent.prompt(batch)` cycle. Lets the host
* clear per-update advisor state — currently the one-advise-per-update gate
* in {@link AdvisorEmissionGuard}, which the host owns because it is the
* one that routes `advise()` results back to the primary.
* clear per-update advisor state and apply the in-progress delivery policy.
* The host owns these gates because it routes `advise()` results back to the
* primary.
*/
beginAdvisorUpdate?(): void;
beginAdvisorUpdate?(inProgress: boolean): void;
/**
* Called with the error of every failed advisor turn, before the retry sleep
* or the dropped-after-3 path. Lets the host apply credential-level remedies
@@ -909,8 +909,8 @@ export class AdvisorRuntime {
const contextWasFresh = resetContext || recoveringOverflow || messageSnapshot === 0;
try {
// Reset the host's per-update advisor state (one-advise-per-update
// gate) before each model cycle so the new batch starts fresh.
this.host.beginAdvisorUpdate?.();
// gate) and pass through whether this batch reviews partial work.
this.host.beginAdvisorUpdate?.(wip);
const prompt = this.agent.prompt(batch);
this.#promptInFlight = prompt;
try {
@@ -877,7 +877,10 @@ export class SessionAdvisors {
this.#maintainAdvisorContext(advisorRef, incomingTokens, signal),
obfuscator: this.#host.obfuscator,
getModelIdentity: () => formatModelString(advisorRef.agent.state.model),
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
beginAdvisorUpdate: inProgress => {
advisorRef.adviseTool.beginUpdate(inProgress);
advisorRef.emissionGuard.beginUpdate();
},
onTurnError: (error, failedMessages, signal) =>
this.#recoverAdvisorTurn(advisorRef, error, failedMessages, signal),
onTurnSuccess: async () => {