Skip to content

Commit 3abdb6e

Browse files
authored
Merge pull request webbrain-one#2738 from esokullu/main
fix: honor complete thread read intent
2 parents 138d029 + 7b48074 commit 3abdb6e

5 files changed

Lines changed: 257 additions & 8 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isSessionQuotaError, serializeConversationForSession, SESSION_CONVERSAT
44
import { formatErrorMessage } from '../error-format.js';
55
import { handleDoneJson } from './cloud-output.js';
66
import { applyReadPageWindow, fitReadPageWindowResult, isReadPageWindowResult } from './read-page-window.js';
7-
import { createReadCompletenessState, isCommunicationThreadContext, readCompletenessBlock, recordReadCompleteness, requiresCompleteThreadRead } from './read-completeness.js';
7+
import { createReadCompletenessState, isCommunicationThreadContext, readCompletenessBlock, recordReadCompleteness, requirePlannerReadCompleteness, requiresCompleteThreadRead } from './read-completeness.js';
88
import { LoopDetector } from './loop-detector.js';
99
import { parseToolCallsFromText } from './tool-call-parser.js';
1010
import { IMAGE_BUDGET, estimateImageTokens, fitImageDimensions } from './image-budget.js';
@@ -664,7 +664,7 @@ export class Agent extends LoopDetector {
664664
const adapterName = getActiveAdapter(pageUrl)?.name || '';
665665
const communicationThread = isCommunicationThreadContext(pageUrl, adapterName);
666666
const required = requiresCompleteThreadRead(userMessage, runOptions, { communicationThread });
667-
this.readCompletenessStates.set(tabId, createReadCompletenessState(token, required));
667+
this.readCompletenessStates.set(tabId, createReadCompletenessState(token, required, communicationThread));
668668
return token;
669669
}
670670

@@ -683,6 +683,14 @@ export class Agent extends LoopDetector {
683683
return next;
684684
}
685685

686+
_armReadCompletenessFromPlan(tabId, plan) {
687+
const state = this.readCompletenessStates.get(tabId);
688+
if (!state) return null;
689+
const next = requirePlannerReadCompleteness(state, plan);
690+
this.readCompletenessStates.set(tabId, next);
691+
return next;
692+
}
693+
686694
_readCompletenessBlock(tabId) {
687695
return readCompletenessBlock(this.readCompletenessStates.get(tabId));
688696
}
@@ -9547,6 +9555,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
95479555
requiresSubmission: plan.request_kind === 'clarify' && plan.requires_submission === true,
95489556
};
95499557
}
9558+
if (!recheckOnly) this._armReadCompletenessFromPlan(tabId, plan);
95509559
return {
95519560
proceed: true,
95529561
requestKind: 'execute',
@@ -9741,6 +9750,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
97419750
onUpdate('plan_auto_approved', { planId, confidence: plan.confidence });
97429751
}
97439752
const approvedScratchpadText = formatPlanScratchpad(plan, '', canonicalVerboseMarkdown);
9753+
this._armReadCompletenessFromPlan(tabId, plan);
97449754
return {
97459755
proceed: true,
97469756
approvedScratchpadText,
@@ -9796,6 +9806,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
97969806
? false
97979807
: approvedSubmissionMetadata;
97989808
const approvedScratchpadText = formatPlanScratchpad(plan, approvedText, canonicalVerboseMarkdown);
9809+
this._armReadCompletenessFromPlan(tabId, approvedText || plan);
97999810
return {
98009811
proceed: true,
98019812
approvedScratchpadText,

src/chrome/src/agent/read-completeness.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,32 @@ export function requiresCompleteThreadRead(userMessage, runOptions = {}, context
4949
|| /\b(?:summari[sz](?:e|ing)|summary|recap|analy[sz](?:e|ing))\b[^.!?\n]{0,80}\b(?:thread|conversation|email\s+chain)\b/i.test(text)
5050
|| /\b(?:follow[- ]?ups?|action\s+items?|open\s+questions?)\b[^.!?\n]{0,80}\b(?:thread|conversation|email\s+chain)\b/i.test(text)
5151
|| /\b(?:thread|conversation|email\s+chain)\b[^.!?\n]{0,50}\b(?:in\s+its\s+entirety|completely|fully|from\s+(?:the\s+)?(?:start|beginning)\s+to\s+(?:the\s+)?end|oldest\s+to\s+newest)\b/i.test(text)
52-
|| /\b(?:read|review(?:ed)?|inspect(?:ed)?|check(?:ed)?|look(?:ed)?\s+at)\b[^.!?\n]{0,80}\b(?:it|this|the\s+(?:thread|conversation|chain))\b[^.!?\n]{0,35}\ball\b/i.test(text);
52+
|| /\b(?:read|review(?:ed)?|inspect(?:ed)?|check(?:ed)?|look(?:ed)?\s+at)\b[^.!?\n]{0,80}\b(?:it|this|the\s+(?:thread|conversation|chain))\b[^.!?\n]{0,35}\ball\b/i.test(text)
53+
|| /\b(?:what(?:'s|\s+is)\s+(?:going\s+on|happening)(?:\s+here)?|what(?:'s|\s+is)\s+(?:this|it)\s+about|catch\s+me\s+up|bring\s+me\s+up\s+to\s+speed)\b/i.test(text)
54+
|| /\bexplain\s+(?:this|it)(?:\s+(?:thread|conversation|email|exchange))?\s*[?.!]*$/i.test(text);
5355
}
5456

55-
export function createReadCompletenessState(runToken = '', required = false) {
57+
export function plannerRequiresCompleteThreadRead(plan = null) {
58+
if (!plan || (typeof plan !== 'string' && plan.request_kind !== 'execute')) return false;
59+
const text = (typeof plan === 'string'
60+
? plan
61+
: [
62+
plan.summary,
63+
...(Array.isArray(plan.steps) ? plan.steps.map(step => step?.action) : []),
64+
].filter(Boolean).join(' ')
65+
).replace(/\s+/g, ' ').trim();
66+
if (!text) return false;
67+
return /\b(?:all|every)\s+(?:of\s+the\s+)?(?:messages?|emails?|replies?)\b/i.test(text)
68+
|| /\b(?:complete|entire|full)\s+(?:email\s+|message\s+)?(?:thread|conversation|chain)\b/i.test(text)
69+
|| /\b(?:read|re-?read|review|inspect|analy[sz]e)\b[^.!?\n]{0,100}\b(?:open|current|visible)\s+(?:gmail\s+|email\s+|message\s+)?(?:thread|conversation)\b/i.test(text)
70+
|| /\b(?:messages?|replies?)\b[^.!?\n]{0,80}\b(?:timestamps?|response\s+timing|reply\s+timing|intervals?)\b/i.test(text)
71+
|| /\b(?:compare|assess|review)\b[^.!?\n]{0,100}\b(?:response|reply)\s+(?:timing|times?|intervals?|speed)\b/i.test(text);
72+
}
73+
74+
export function createReadCompletenessState(runToken = '', required = false, communicationThread = false) {
5675
return {
5776
runToken: String(runToken || ''),
77+
communicationThread: communicationThread === true,
5878
required: required === true,
5979
sawEligibleRead: false,
6080
complete: required !== true,
@@ -66,6 +86,22 @@ export function createReadCompletenessState(runToken = '', required = false) {
6686
};
6787
}
6888

89+
export function requirePlannerReadCompleteness(state, plan = null) {
90+
const current = state || createReadCompletenessState();
91+
if (!current.communicationThread || current.required || !plannerRequiresCompleteThreadRead(plan)) return current;
92+
return {
93+
...current,
94+
required: true,
95+
complete: false,
96+
sawEligibleRead: false,
97+
treeKey: '',
98+
treePages: [],
99+
treeTerminalPage: null,
100+
pendingTool: '',
101+
continuationArgs: null,
102+
};
103+
}
104+
69105
function accessibilityTreeState(state, args, result) {
70106
const filter = String(args?.filter || 'all');
71107
const maxDepth = args?.maxDepth == null ? 15 : Number(args.maxDepth);

src/firefox/src/agent/agent.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isSessionQuotaError, serializeConversationForSession, SESSION_CONVERSAT
44
import { formatErrorMessage } from '../error-format.js';
55
import { handleDoneJson } from './cloud-output.js';
66
import { applyReadPageWindow, fitReadPageWindowResult, isReadPageWindowResult } from './read-page-window.js';
7-
import { createReadCompletenessState, isCommunicationThreadContext, readCompletenessBlock, recordReadCompleteness, requiresCompleteThreadRead } from './read-completeness.js';
7+
import { createReadCompletenessState, isCommunicationThreadContext, readCompletenessBlock, recordReadCompleteness, requirePlannerReadCompleteness, requiresCompleteThreadRead } from './read-completeness.js';
88
import { LoopDetector } from './loop-detector.js';
99
import { parseToolCallsFromText } from './tool-call-parser.js';
1010
import { IMAGE_BUDGET, estimateImageTokens, fitImageDimensions } from './image-budget.js';
@@ -589,7 +589,7 @@ export class Agent extends LoopDetector {
589589
const adapterName = getActiveAdapter(pageUrl)?.name || '';
590590
const communicationThread = isCommunicationThreadContext(pageUrl, adapterName);
591591
const required = requiresCompleteThreadRead(userMessage, runOptions, { communicationThread });
592-
this.readCompletenessStates.set(tabId, createReadCompletenessState(token, required));
592+
this.readCompletenessStates.set(tabId, createReadCompletenessState(token, required, communicationThread));
593593
return token;
594594
}
595595

@@ -608,6 +608,14 @@ export class Agent extends LoopDetector {
608608
return next;
609609
}
610610

611+
_armReadCompletenessFromPlan(tabId, plan) {
612+
const state = this.readCompletenessStates.get(tabId);
613+
if (!state) return null;
614+
const next = requirePlannerReadCompleteness(state, plan);
615+
this.readCompletenessStates.set(tabId, next);
616+
return next;
617+
}
618+
611619
_readCompletenessBlock(tabId) {
612620
return readCompletenessBlock(this.readCompletenessStates.get(tabId));
613621
}
@@ -8424,6 +8432,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
84248432
requiresSubmission: plan.request_kind === 'clarify' && plan.requires_submission === true,
84258433
};
84268434
}
8435+
if (!recheckOnly) this._armReadCompletenessFromPlan(tabId, plan);
84278436
return {
84288437
proceed: true,
84298438
requestKind: 'execute',
@@ -8614,6 +8623,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
86148623
onUpdate('plan_auto_approved', { planId, confidence: plan.confidence });
86158624
}
86168625
const approvedScratchpadText = formatPlanScratchpad(plan, '', canonicalVerboseMarkdown);
8626+
this._armReadCompletenessFromPlan(tabId, plan);
86178627
return {
86188628
proceed: true,
86198629
approvedScratchpadText,
@@ -8669,6 +8679,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
86698679
? false
86708680
: approvedSubmissionMetadata;
86718681
const approvedScratchpadText = formatPlanScratchpad(plan, approvedText, canonicalVerboseMarkdown);
8682+
this._armReadCompletenessFromPlan(tabId, approvedText || plan);
86728683
return {
86738684
proceed: true,
86748685
approvedScratchpadText,

src/firefox/src/agent/read-completeness.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,32 @@ export function requiresCompleteThreadRead(userMessage, runOptions = {}, context
4949
|| /\b(?:summari[sz](?:e|ing)|summary|recap|analy[sz](?:e|ing))\b[^.!?\n]{0,80}\b(?:thread|conversation|email\s+chain)\b/i.test(text)
5050
|| /\b(?:follow[- ]?ups?|action\s+items?|open\s+questions?)\b[^.!?\n]{0,80}\b(?:thread|conversation|email\s+chain)\b/i.test(text)
5151
|| /\b(?:thread|conversation|email\s+chain)\b[^.!?\n]{0,50}\b(?:in\s+its\s+entirety|completely|fully|from\s+(?:the\s+)?(?:start|beginning)\s+to\s+(?:the\s+)?end|oldest\s+to\s+newest)\b/i.test(text)
52-
|| /\b(?:read|review(?:ed)?|inspect(?:ed)?|check(?:ed)?|look(?:ed)?\s+at)\b[^.!?\n]{0,80}\b(?:it|this|the\s+(?:thread|conversation|chain))\b[^.!?\n]{0,35}\ball\b/i.test(text);
52+
|| /\b(?:read|review(?:ed)?|inspect(?:ed)?|check(?:ed)?|look(?:ed)?\s+at)\b[^.!?\n]{0,80}\b(?:it|this|the\s+(?:thread|conversation|chain))\b[^.!?\n]{0,35}\ball\b/i.test(text)
53+
|| /\b(?:what(?:'s|\s+is)\s+(?:going\s+on|happening)(?:\s+here)?|what(?:'s|\s+is)\s+(?:this|it)\s+about|catch\s+me\s+up|bring\s+me\s+up\s+to\s+speed)\b/i.test(text)
54+
|| /\bexplain\s+(?:this|it)(?:\s+(?:thread|conversation|email|exchange))?\s*[?.!]*$/i.test(text);
5355
}
5456

55-
export function createReadCompletenessState(runToken = '', required = false) {
57+
export function plannerRequiresCompleteThreadRead(plan = null) {
58+
if (!plan || (typeof plan !== 'string' && plan.request_kind !== 'execute')) return false;
59+
const text = (typeof plan === 'string'
60+
? plan
61+
: [
62+
plan.summary,
63+
...(Array.isArray(plan.steps) ? plan.steps.map(step => step?.action) : []),
64+
].filter(Boolean).join(' ')
65+
).replace(/\s+/g, ' ').trim();
66+
if (!text) return false;
67+
return /\b(?:all|every)\s+(?:of\s+the\s+)?(?:messages?|emails?|replies?)\b/i.test(text)
68+
|| /\b(?:complete|entire|full)\s+(?:email\s+|message\s+)?(?:thread|conversation|chain)\b/i.test(text)
69+
|| /\b(?:read|re-?read|review|inspect|analy[sz]e)\b[^.!?\n]{0,100}\b(?:open|current|visible)\s+(?:gmail\s+|email\s+|message\s+)?(?:thread|conversation)\b/i.test(text)
70+
|| /\b(?:messages?|replies?)\b[^.!?\n]{0,80}\b(?:timestamps?|response\s+timing|reply\s+timing|intervals?)\b/i.test(text)
71+
|| /\b(?:compare|assess|review)\b[^.!?\n]{0,100}\b(?:response|reply)\s+(?:timing|times?|intervals?|speed)\b/i.test(text);
72+
}
73+
74+
export function createReadCompletenessState(runToken = '', required = false, communicationThread = false) {
5675
return {
5776
runToken: String(runToken || ''),
77+
communicationThread: communicationThread === true,
5878
required: required === true,
5979
sawEligibleRead: false,
6080
complete: required !== true,
@@ -66,6 +86,22 @@ export function createReadCompletenessState(runToken = '', required = false) {
6686
};
6787
}
6888

89+
export function requirePlannerReadCompleteness(state, plan = null) {
90+
const current = state || createReadCompletenessState();
91+
if (!current.communicationThread || current.required || !plannerRequiresCompleteThreadRead(plan)) return current;
92+
return {
93+
...current,
94+
required: true,
95+
complete: false,
96+
sawEligibleRead: false,
97+
treeKey: '',
98+
treePages: [],
99+
treeTerminalPage: null,
100+
pendingTool: '',
101+
continuationArgs: null,
102+
};
103+
}
104+
69105
function accessibilityTreeState(state, args, result) {
70106
const filter = String(args?.filter || 'all');
71107
const maxDepth = args?.maxDepth == null ? 15 : Number(args.maxDepth);

0 commit comments

Comments
 (0)