@kkeve/suite 0.0.12 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,6 +61,91 @@ const tools_1 = require("./tools");
61
61
  const config_1 = require("../config");
62
62
  // ─── System Prompt ────────────────────────────────────────────────────
63
63
  const SYSTEM_PROMPT = fs.readFileSync(path.join(__dirname, "system_prompt.md"), "utf-8");
64
+ // ─── Snapshot diff helpers (used by assembleUserPrompt to add Page Change to history) ───
65
+ /** Find the snapshot string of the previous step event before index `i`. */
66
+ function findPrevStepSnapshot(events, currentIdx) {
67
+ for (let j = currentIdx - 1; j >= 0; j--) {
68
+ if (events[j].type === 'step' && events[j].snapshot)
69
+ return events[j].snapshot;
70
+ }
71
+ return undefined;
72
+ }
73
+ /**
74
+ * Summarize key differences between two a11y snapshots.
75
+ * Returns a short human-readable string or '' if nothing notable changed.
76
+ *
77
+ * Detects: dialog/modal appearance/disappearance, new/removed elements,
78
+ * value changes in inputs, and repeated identical snapshots (no change).
79
+ */
80
+ function summarizeSnapshotDiff(prev, curr) {
81
+ if (prev === curr)
82
+ return 'no visible change on page';
83
+ const prevLines = prev.split('\n');
84
+ const currLines = curr.split('\n');
85
+ const changes = [];
86
+ // 1. Detect dialog/modal appearance or disappearance
87
+ const prevDialogs = prevLines.filter(l => /dialog|modal|alertdialog/i.test(l) && /\[ref=/.test(l));
88
+ const currDialogs = currLines.filter(l => /dialog|modal|alertdialog/i.test(l) && /\[ref=/.test(l));
89
+ if (currDialogs.length > prevDialogs.length) {
90
+ const newDialog = currDialogs.find(cd => !prevDialogs.some(pd => pd === cd));
91
+ if (newDialog) {
92
+ const nameMatch = newDialog.match(/"([^"]+)"/);
93
+ changes.push(`dialog "${nameMatch?.[1] || '?'}" appeared`);
94
+ }
95
+ }
96
+ else if (currDialogs.length < prevDialogs.length) {
97
+ changes.push('dialog closed');
98
+ }
99
+ // 2. Detect new warning/error text nodes (e.g. validation errors)
100
+ const prevWarnings = prevLines.filter(l => /warning|error|alert|⚠|❌/i.test(l) && /\[ref=/.test(l));
101
+ const currWarnings = currLines.filter(l => /warning|error|alert|⚠|❌/i.test(l) && /\[ref=/.test(l));
102
+ for (const w of currWarnings) {
103
+ if (!prevWarnings.some(pw => pw === w)) {
104
+ const textMatch = w.match(/"([^"]+)"/);
105
+ changes.push(`new warning/error: "${textMatch?.[1] || '?'}"`);
106
+ }
107
+ }
108
+ // 3. Count added/removed interactive elements (simplified: lines with [ref=])
109
+ const prevRefs = new Set(prevLines.filter(l => /\[ref=/.test(l)).map(l => {
110
+ const m = l.match(/\[ref=([a-f0-9]+)\]/);
111
+ return m?.[1] || '';
112
+ }));
113
+ const currRefs = new Set(currLines.filter(l => /\[ref=/.test(l)).map(l => {
114
+ const m = l.match(/\[ref=([a-f0-9]+)\]/);
115
+ return m?.[1] || '';
116
+ }));
117
+ const added = [...currRefs].filter(r => r && !prevRefs.has(r));
118
+ const removed = [...prevRefs].filter(r => r && !currRefs.has(r));
119
+ if (added.length > 0)
120
+ changes.push(`${added.length} new element(s) appeared`);
121
+ if (removed.length > 0)
122
+ changes.push(`${removed.length} element(s) removed`);
123
+ // 4. Detect value changes in textboxes (content after colon on same line)
124
+ const prevValues = new Map();
125
+ for (const l of prevLines) {
126
+ const refM = l.match(/\[ref=([a-f0-9]+)\]/);
127
+ if (refM) {
128
+ // Value is text after the last colon on the same line
129
+ const valM = l.match(/: (.+)$/);
130
+ if (valM)
131
+ prevValues.set(refM[1], valM[1].trim());
132
+ }
133
+ }
134
+ for (const l of currLines) {
135
+ const refM = l.match(/\[ref=([a-f0-9]+)\]/);
136
+ if (refM) {
137
+ const valM = l.match(/: (.+)$/);
138
+ if (valM && prevValues.has(refM[1])) {
139
+ const prevVal = prevValues.get(refM[1]);
140
+ const currVal = valM[1].trim();
141
+ if (prevVal !== currVal && currVal) {
142
+ changes.push(`[ref=${refM[1]}] value changed: "${prevVal}" → "${currVal}"`);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ return changes.length > 0 ? changes.join('; ') : 'page content changed';
148
+ }
64
149
  // ─── KevePageAgent ──────────────────────────────────────────────────────
65
150
  class KevePageAgent {
66
151
  page;
@@ -72,6 +157,13 @@ class KevePageAgent {
72
157
  /** Current execute options (set at start of execute(), accessible from hooks) */
73
158
  options;
74
159
  hooks;
160
+ // Page event listener state (used by handle_dialog, console_messages, file_upload, network_requests)
161
+ _dialogHandler = null;
162
+ _lastDialogInfo = null;
163
+ _pendingFileChooser = null;
164
+ _consoleMessages = [];
165
+ _networkRequests = [];
166
+ _pageListenersSetup = false;
75
167
  constructor(page, options) {
76
168
  this.page = page;
77
169
  this.maxSteps = options?.maxSteps ?? 8;
@@ -90,12 +182,95 @@ class KevePageAgent {
90
182
  stop() {
91
183
  this.abortController.abort();
92
184
  }
185
+ /** Set up page event listeners for dialog, console, network, and file chooser capture.
186
+ * Called once per page; subsequent calls are no-ops. */
187
+ setupPageListeners() {
188
+ if (this._pageListenersSetup)
189
+ return;
190
+ this._pageListenersSetup = true;
191
+ // ── Dialog handler ──
192
+ // Playwright auto-dismisses dialogs unless a handler is registered.
193
+ // Our handler auto-accepts by default, but _dialogHandler can override.
194
+ this.page.on('dialog', async (dialog) => {
195
+ const handler = this._dialogHandler;
196
+ const accept = handler?.accept ?? true;
197
+ const promptText = handler?.promptText;
198
+ try {
199
+ if (accept) {
200
+ await dialog.accept(promptText || '');
201
+ }
202
+ else {
203
+ await dialog.dismiss();
204
+ }
205
+ }
206
+ catch {
207
+ // Fallback: always accept to unblock the page
208
+ try {
209
+ await dialog.accept();
210
+ }
211
+ catch { /* give up */ }
212
+ }
213
+ this._lastDialogInfo = {
214
+ type: dialog.type(),
215
+ message: dialog.message(),
216
+ accepted: accept,
217
+ };
218
+ this._dialogHandler = null;
219
+ // Notify the Agent so it knows a dialog appeared
220
+ this.pushObservation(`🔔 Native dialog [${dialog.type()}] appeared: "${dialog.message().slice(0, 100)}" — was ${accept ? 'accepted' : 'dismissed'}.`);
221
+ });
222
+ // ── Console messages ──
223
+ this.page.on('console', (msg) => {
224
+ this._consoleMessages.push({ level: msg.type(), text: msg.text() });
225
+ if (this._consoleMessages.length > 200)
226
+ this._consoleMessages.shift();
227
+ });
228
+ this.page.on('pageerror', (err) => {
229
+ this._consoleMessages.push({ level: 'error', text: `Uncaught: ${err.message}` });
230
+ });
231
+ // ── Network requests ──
232
+ this.page.on('request', (req) => {
233
+ const url = req.url();
234
+ const isStatic = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|map)(\?|$)/i.test(url);
235
+ this._networkRequests.push({
236
+ _req: req,
237
+ method: req.method(),
238
+ url,
239
+ isStatic,
240
+ status: null,
241
+ });
242
+ if (this._networkRequests.length > 100)
243
+ this._networkRequests.shift();
244
+ });
245
+ this.page.on('response', (res) => {
246
+ const req = res.request();
247
+ // Match by reference identity — Playwright guarantees same Request object
248
+ for (let i = this._networkRequests.length - 1; i >= 0; i--) {
249
+ if (this._networkRequests[i]._req === req) {
250
+ this._networkRequests[i].status = res.status();
251
+ break;
252
+ }
253
+ }
254
+ });
255
+ // ── File chooser ──
256
+ this.page.on('filechooser', (fileChooser) => {
257
+ this._pendingFileChooser = fileChooser;
258
+ });
259
+ }
93
260
  /** Main Re-Act execute loop */
94
261
  async execute(step, expected, options) {
95
262
  // Reset state
96
263
  this.events = [];
97
264
  this.abortController = new AbortController();
98
265
  this.options = options;
266
+ // Reset page event listener state
267
+ this._dialogHandler = null;
268
+ this._lastDialogInfo = null;
269
+ this._pendingFileChooser = null;
270
+ this._consoleMessages = [];
271
+ this._networkRequests = [];
272
+ // Set up page event listeners (idempotent — only runs once per page)
273
+ this.setupPageListeners();
99
274
  const hooks = options?.hooks ?? this.hooks;
100
275
  // Link external signal (e.g. per-goal timeout) to internal abort controller
101
276
  const externalSignal = options?.signal;
@@ -115,7 +290,7 @@ class KevePageAgent {
115
290
  this.pushObservation(`Target application URL: ${targetUrl}. Use this URL for navigation. Do NOT construct URLs yourself — use the navigate tool with this URL or read process.env.KEVE_TARGET_URL.`);
116
291
  }
117
292
  if (options?.learnedActionsHint) {
118
- this.pushObservation(`Previous discoveries for similar steps:\n${options.learnedActionsHint}`);
293
+ this.pushObservation(`Previous discoveries for similar steps:\n${options.learnedActionsHint}\nUse these discoveries to avoid repeating failed approaches. If a previous step found a working way to fill a form field (especially combobox/dropdown), reuse that approach instead of trial-and-error.`);
119
294
  }
120
295
  // Step limit warning
121
296
  if (maxSteps <= 3) {
@@ -209,10 +384,12 @@ class KevePageAgent {
209
384
  if (actionName === 'execute_javascript' && execResult.output && execResult.output.startsWith('❌')) {
210
385
  const jsRulesHint = [
211
386
  'execute_javascript rules — fix your script:',
212
- '1. MUST start with `return` to capture the result.',
213
- '2. Use `var` for declarations — `const`/`let` cause SyntaxError.',
387
+ '1. Include `return` for multi-statement code. Single expressions are auto-wrapped.',
388
+ '2. const/let are auto-converted to var — no need to write var manually.',
214
389
  '3. NO top-level `await` or `async () => {}` wrappers — they return undefined.',
215
390
  '4. NO Playwright APIs (`page`, `locator`) — only browser DOM APIs.',
391
+ '5. FORM INTERACTION allowed: input.value=, dispatchEvent().',
392
+ '6. FORBIDDEN: style mutation (style.xxx=, style.setProperty), classList mutation, className=, innerHTML=.',
216
393
  ].join('\n');
217
394
  this.pushObservation(jsRulesHint);
218
395
  }
@@ -296,6 +473,53 @@ class KevePageAgent {
296
473
  await hooks?.onAfterStep?.(this, this.events);
297
474
  }
298
475
  stepCount++;
476
+ // ── Stuck detection: break retry loops early ──
477
+ // When the last 3 steps produce identical toolOutput (no page progress),
478
+ // inject a forceful observation to break the loop.
479
+ // Also detects oscillation patterns (e.g. A→B→A→B cycling between two states).
480
+ const recentSteps = this.events
481
+ .filter((e) => e.type === 'step')
482
+ .slice(-4);
483
+ if (recentSteps.length >= 3) {
484
+ const last3 = recentSteps.slice(-3);
485
+ const normalize = (s) => (s || '').replace(/executed for \d+ms/, '').slice(0, 80);
486
+ const outputs = last3.map(s => normalize(s.toolOutput));
487
+ const allSame = outputs.every(o => o === outputs[0]);
488
+ if (allSame) {
489
+ const stuckMsg = [
490
+ `⚠️ STUCK DETECTED: The last 3 actions all produced the same result: "${outputs[0].slice(0, 60)}"`,
491
+ `This means the page is NOT responding to your actions. You MUST change strategy:`,
492
+ `- Try a different tool (e.g. select_option instead of fill_form, execute_javascript instead of click)`,
493
+ `- Try a different element or value`,
494
+ `- If a dialog blocks submission, handle the sub-dialog/popup first or dismiss it with Escape`,
495
+ `- If stuck after 2 more attempts: call done(verdict="fail") honestly`,
496
+ ].join('\n');
497
+ this.pushObservation(stuckMsg);
498
+ }
499
+ // ── Oscillation detection: check if last 6 steps cycle between 2-3 states ──
500
+ // E.g. click确定→ownership popup → click返回→conflict warning → click确定→ownership popup
501
+ const recent6 = this.events
502
+ .filter((e) => e.type === 'step')
503
+ .slice(-6);
504
+ if (recent6.length >= 5 && !allSame) {
505
+ const norm6 = recent6.map(s => normalize(s.toolOutput));
506
+ // Extract the set of unique outputs (ignoring minor variations)
507
+ const uniqueOutputs = [...new Set(norm6)];
508
+ if (uniqueOutputs.length <= 2) {
509
+ // All recent steps cycle between ≤2 distinct outputs → oscillation
510
+ const oscMsg = [
511
+ `⚠️ OSCILLATION DETECTED: The last ${recent6.length} actions cycle between ${uniqueOutputs.length} states without progress:`,
512
+ ...uniqueOutputs.map(o => ` - "${o.slice(0, 50)}"`),
513
+ `You are going back and forth without advancing. You MUST break the cycle:`,
514
+ `- If clicking 确定 triggers a repeated warning, change the FORM VALUES (not just the name) to resolve the warning`,
515
+ `- If "口径重复" blocks submission, change the DATASET or INDICATOR (not just the indicator name)`,
516
+ `- If a sub-dialog keeps appearing, dismiss it with Escape or handle it first`,
517
+ `- Call done(verdict="fail") honestly if the conflict cannot be resolved within current steps`,
518
+ ].join('\n');
519
+ this.pushObservation(oscMsg);
520
+ }
521
+ }
522
+ }
299
523
  if (stepCount >= maxSteps) {
300
524
  console.error(`\x1b[31mStep count exceeded maximum limit (${maxSteps})\x1b[0m`);
301
525
  const finalSnapshot = await this.page.ariaSnapshot();
@@ -357,13 +581,21 @@ class KevePageAgent {
357
581
  prompt += '</agent_state>\n\n';
358
582
  // <agent_history>
359
583
  prompt += '<agent_history>\n';
360
- for (const event of this.events) {
584
+ for (let i = 0; i < this.events.length; i++) {
585
+ const event = this.events[i];
361
586
  if (event.type === 'step') {
362
587
  prompt += `<step_${event.stepIndex + 1}>\n`;
363
588
  prompt += `Evaluation of Previous Step: ${event.evaluation}\n`;
364
589
  prompt += `Memory: ${event.memory}\n`;
365
590
  prompt += `Next Goal: ${event.nextGoal}\n`;
366
591
  prompt += `Action: ${event.toolName} → ${event.toolOutput || event.toolError || 'unknown'}\n`;
592
+ // Add snapshot diff summary: compare this step's snapshot with the previous step's
593
+ const prevStep = i > 0 ? findPrevStepSnapshot(this.events, i) : undefined;
594
+ if (prevStep !== undefined) {
595
+ const diff = summarizeSnapshotDiff(prevStep, event.snapshot);
596
+ if (diff)
597
+ prompt += `Page Change: ${diff}\n`;
598
+ }
367
599
  prompt += `</step_${event.stepIndex + 1}>\n`;
368
600
  }
369
601
  else if (event.type === 'observation') {
@@ -496,6 +728,52 @@ IMPORTANT: Respond in Chinese.`,
496
728
  return { found: false, analysis: `LLM invoke error: ${e.message}` };
497
729
  }
498
730
  }
731
+ /** Visual assertion: check a visual condition without interacting with the page */
732
+ async visualAssertElement(screenshotBase64, assertion) {
733
+ const assertToolSchema = zod_1.z.object({
734
+ passed: zod_1.z.boolean().describe('Whether the assertion is true based on the screenshot'),
735
+ reasoning: zod_1.z.string().describe('Brief explanation of why the assertion passed or failed'),
736
+ });
737
+ const messages = [
738
+ {
739
+ role: 'system',
740
+ content: `You are a UI visual assertion checker. Given a screenshot and an assertion statement, determine whether the assertion is TRUE based on what you see.
741
+
742
+ Output JSON with:
743
+ - passed: true if the assertion holds, false otherwise
744
+ - reasoning: brief explanation of your judgment
745
+
746
+ IMPORTANT:
747
+ - Only judge what you can SEE in the screenshot — do not infer hidden states
748
+ - Be strict: if the visual evidence is ambiguous, return passed=false
749
+ - Respond in Chinese for reasoning`,
750
+ },
751
+ {
752
+ role: 'user',
753
+ content: [
754
+ { type: 'text', text: `请判断以下断言是否为真:${assertion}\n\n返回 passed (布尔值) 和 reasoning (中文说明)。` },
755
+ { type: 'image_url', image_url: { url: `data:image/png;base64,${screenshotBase64}`, detail: 'high' } },
756
+ ],
757
+ },
758
+ ];
759
+ try {
760
+ const result = await this.llm.invoke(messages, {
761
+ AssertResult: {
762
+ description: 'Return the visual assertion result',
763
+ inputSchema: assertToolSchema,
764
+ execute: async (args) => args,
765
+ },
766
+ }, this.abortController.signal, { toolChoiceName: 'AssertResult' });
767
+ const parsed = result.toolCall.args;
768
+ return {
769
+ passed: typeof parsed.passed === 'boolean' ? parsed.passed : false,
770
+ reasoning: parsed.reasoning || 'No reasoning provided',
771
+ };
772
+ }
773
+ catch (e) {
774
+ return { passed: false, reasoning: `LLM invoke error: ${e.message}` };
775
+ }
776
+ }
499
777
  /** Build final result */
500
778
  buildResult(success, data, stepCount, finalSnapshot) {
501
779
  const snapshot = finalSnapshot || '';
@@ -11,6 +11,15 @@ You excel at following tasks:
11
11
  6. Using page screenshots as the primary visual source of truth
12
12
  </intro>
13
13
 
14
+ <capability>
15
+ - It is ok to fail the task. If the expected state cannot be achieved due to page bugs, test data issues, or validation blocks, call done(verdict="fail") honestly rather than looping indefinitely.
16
+ - Trying too hard can be harmful. Repeating the same action back and forth (especially clicking 确定/提交 on a form that won't submit) wastes steps and causes unwanted side-effects. When stuck, call done with an honest verdict.
17
+ - The page can be broken. If the page has bugs making the expected state unreachable (e.g. validation warning "口径与已有指标重复" that cannot be resolved within current context), report it as fail or blocked — do NOT keep retrying.
18
+ - If a click tool returns ✅ with "auto-dismissed" message, the sub-dialog was automatically dismissed and the original click was retried. Check the result: if it says "dialog closed", the form was submitted successfully; if it says "dialog still open", the retried click triggered another issue (e.g. validation error or another popup) — read the message and handle it.
19
+ - **Sub-dialog / popup handling**: When clicking a dialog button triggers a NEW popup (⚠️ shows "new popup: ..."), this means a sub-dialog appeared on top. You MUST handle the sub-dialog FIRST before retrying the original action. Read the accessibility tree to see the sub-dialog's fields and buttons, fill/interact with them, then close/confirm the sub-dialog. After the sub-dialog closes, the original dialog should now accept your action.
20
+ - **Unrelated intercepting popups**: If a popup unrelated to the form validation appears (e.g. ownership transfer, assignment, notification), dismiss it by pressing Escape or clicking its cancel/close button, then proceed with the original action.
21
+ </capability>
22
+
14
23
  <language_settings>
15
24
  - Default working language: **中文**
16
25
  - Use the same language as the user request.
@@ -84,6 +93,13 @@ Important notes:
84
93
  - If an expected element is not visible, try scrolling or navigating
85
94
  - The `ref` values are stable within a page state but change after navigation — always use the latest snapshot
86
95
  - The screenshot shows the actual visual state — use it to verify visual expectations (colors, layout, visibility, text rendering) that the accessibility tree may not capture
96
+
97
+ **Combobox/dropdown interaction strategy:**
98
+ - For combobox/dropdown fields, ALWAYS use `select_option` tool (or `fill_form` with type="combobox") — these tools handle custom components (ks-select, ant-select, el-select) automatically.
99
+ - If a combobox selection returns "value did not update", do NOT retry the same approach — the tool already applied a type+Enter fallback. Move on to the next step.
100
+ - If a dropdown option causes a duplicate/conflict warning, immediately select a DIFFERENT option — do NOT re-select the same one.
101
+ - When filling a form with multiple combobox fields, fill one at a time (each combobox opens/closes a dropdown), not all at once.
102
+ - **CRITICAL**: Never repeat the same combobox action more than 2 times. If it fails twice, use `execute_javascript` to set the value programmatically, or call `done(verdict="fail")` honestly.
87
103
  - `[cursor=pointer]` indicates the element is clickable
88
104
  - iframe content is fully visible in the tree — no special treatment needed
89
105
  </browser_state>
@@ -115,8 +131,42 @@ Strictly follow these rules while using the browser:
115
131
  - If the page is not fully loaded, use the `wait` action.
116
132
  - Do not repeat the same action more than 3 times unless conditions changed.
117
133
  - If a click doesn't produce the expected result, try an alternative element or approach.
118
- - When typing into a field, use the `type` action which clears existing content first.
134
+ - When typing into a field, use the `type` action. By default it clears existing content first (mode="replace"). Use mode="append" to add text without clearing.
119
135
  - Before calling done(verdict="pass"), cross-reference the screenshot with the accessibility tree and the expected state. Only call done(verdict="pass") when you are confident the expected state is fully achieved.
136
+
137
+ <form_rules>
138
+ **Form filling best practices:**
139
+ 1. Use `fill_form` for multi-field forms — it fills textbox/checkbox/radio/slider AND combobox/dropdown fields in one step. For combobox, it automatically handles both native `<select>` and custom components (ks-select/ant-select) via click-open→click-option, with type+Enter fallback when click-option fails. This reduces step count and prevents missing required fields.
140
+ 2. For a single standalone dropdown selection (not part of a multi-field form), use `select_option` instead — it does the same combobox handling in one step.
141
+ 3. **Do NOT use `type` tool directly on a combobox/dropdown field** — the `type` tool auto-detects combobox and delegates to `select_option`, but if you use it explicitly, it signals you're trying to type free text into a dropdown, which is usually wrong. Use `fill_form` or `select_option` instead.
142
+ 4. Typical workflow: one `fill_form` call with ALL fields (including combobox), then click 确定/提交. Be prepared for post-submit issues (duplicate warnings, intercepting popups) — handle them before retrying.
143
+ 5. Before clicking 确定/提交/Save, check ALL required fields are filled — not just the field mentioned in the current step. Missing required fields is the #1 reason forms fail to submit.
144
+ 6. Never click 确定/提交 more than 2 times without verifying state change. If the dialog stays open, check for validation errors or intercepting sub-dialogs (see validation detection below).
145
+ 7. **"留空" means DON'T fill that field** — do NOT use fill_form with an empty string ("") for a field that should be left empty. Simply omit that field from the fill_form fields array. Only include fields that need actual values.
146
+
147
+ **Validation state detection:**
148
+ - The a11y tree often does NOT show validation state (aria-invalid is usually absent for custom components).
149
+ - When the expected says "字段标红", "提示必填", "显示错误提示", use `visual_assert` to check the visual condition — it screenshots and analyzes the page without interacting.
150
+ - Alternatively, `execute_javascript` can query CSS properties (e.g. `getComputedStyle(el).borderColor`) for precise values.
151
+ - Do NOT use `visual_locate` for validation checks — it CLICKS and will alter page state.
152
+ - If clicking 确定/提交 keeps the dialog open with no visible change, that itself indicates validation failure.
153
+
154
+ **Reusing discoveries from previous steps (learned hints):**
155
+ - If a <sys> message contains "Previous discoveries for SIMILAR step", it describes actions that worked for a similar form on the same page.
156
+ - Pay special attention to "Key discoveries" — these show which combobox options were successfully selected (e.g. `✅ combobox = "KwaiBI官方数据集DEMO" (custom)`) and which dialog warnings appeared (e.g. `⚠️ ...new popup: "口径与已有指标重复"`).
157
+ - If the hint shows a working option for a combobox you need to fill, reuse that exact option value instead of guessing.
158
+ - If the hint shows a dialog warning that blocked submission, you already know about it — adjust your strategy accordingly.
159
+
160
+ **Character length for input validation:**
161
+ - When a test case specifies a character limit (e.g. "50个字符", "最大300字"), each Chinese character counts as 1 character — same as `.length` in JavaScript.
162
+ - Do NOT convert character limits to byte counts (e.g. "50字符限制→输入25个中文" is WRONG).
163
+ - To test a "50字符" limit, type exactly 50 Chinese characters for the boundary test. For the over-limit test, type 51 Chinese characters.
164
+ - Example: if expected says "输入不超过50字符", type 50个中文汉字 to verify the upper bound is accepted, and 51个中文汉字 to verify rejection.
165
+
166
+ **Important:**
167
+ - The `[ref=xxx]` identifier is for click/type/hover tools only (aria-ref selector). It does NOT exist as a DOM attribute — never use `document.querySelector('[ref=xxx]')`.
168
+ - When using `type` with mode="append", text is added after existing content without clearing. Default mode="replace" clears first.
169
+ </form_rules>
120
170
  </browser_rules>
121
171
 
122
172
  <task_completion_rules>
@@ -149,7 +199,7 @@ When the `navigate` tool returns an error indicating a missing/empty environment
149
199
  - Do NOT call done with verdict="pass" if the screenshot shows the expected state is NOT achieved.
150
200
  - **When to stop immediately**: If `execute_javascript` or another deterministic tool returns a result that definitively proves the expected state CANNOT be achieved (e.g. computed style value mismatch), call `done` with the appropriate verdict right away. Do NOT continue exploring, scrolling, or navigating — more actions will not change a CSS computed value or a DOM property. Wasting steps on already-failed assertions reduces test reliability.
151
201
  - **When prior_execution shows the conclusion**: If the `<prior_execution>` block shows an assertion failure with `Expected:` and `Received:` values, the deterministic script has already captured the precise value. You may use `execute_javascript` once to confirm the current value, then immediately call `done(verdict="fail")` with the actual vs expected values in `text`. Do NOT explore further — the assertion already proved the mismatch.
152
- - **execute_javascript is ONLY for reading precise values**: Use it to query computed styles, DOM properties, or measurements that screenshots/accessibility tree cannot provide. Do NOT use it for clicking, typing, navigating, form filling, or any page interaction — use the dedicated tools (click, type, navigate) instead. Do NOT use it to repeat what the accessibility tree already shows. Do NOT use it for logic/assertions — just return the raw value and judge the result yourself.
202
+ - **execute_javascript is primarily for reading precise values**: Use it to query computed styles, DOM properties, or measurements that screenshots/accessibility tree cannot provide. Do NOT use it for typing, navigating, or form filling — use the dedicated tools (type, navigate, fill_form) instead. Do NOT use it to repeat what the accessibility tree already shows. Do NOT use it for logic/assertions — just return the raw value and judge the result yourself. **EXCEPTION — overlay bypass**: When the click tool is blocked by overlay interception (returns ⚠️ with "dialog still open" repeatedly for the same element), you MAY use `execute_javascript` with `element.click()` to bypass the overlay. In this case, locate the element via its accessible name/role/text (e.g., `document.querySelector('button[aria-label="确定"]')` or `Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('确定'))`), NOT via `[ref=xxx]` which is NOT a DOM attribute.
153
203
  - **NEVER modify the page under test (CRITICAL RULE)**: You are a TESTER — your job is to OBSERVE and REPORT the actual state of the page, never to ALTER it to match expectations. If the actual value differs from the expected value, that is a TEST FAILURE — report it honestly as `done(verdict="fail")`. Do NOT use `execute_javascript`, `click`, `type`, or any other tool to "fix" the page so the test passes. Specifically:
154
204
  - Do NOT inject CSS (style.setProperty, style overrides, CSS variable changes)
155
205
  - Do NOT modify DOM structure (adding/removing elements)
@@ -157,12 +207,15 @@ When the `navigate` tool returns an error indicating a missing/empty environment
157
207
  - Do NOT override computed styles to match expected values
158
208
  - If `execute_javascript` returns a value that differs from expected, call `done(verdict="fail")` with the actual vs expected — do NOT attempt to change the value
159
209
  - **execute_javascript <rule>**:
160
- 1. Script MUST start with `return` — no auto-wrapping. Omitting `return` causes execution rejection.
161
- 2. Use `var` for variable declarations. `const`/`let` cause SyntaxError in page.evaluate context.
162
- 3. NO top-level `await` — the script runs in a synchronous IIFE. There is no async context.
163
- 4. NO `async () => {}` wrappers — they return undefined. Write flat statements ending with `return`.
164
- 5. NO Playwright APIs (`page`, `locator`, `expect`) — only browser DOM APIs exist inside the page.
165
- 6. Read-only: do NOT modify DOM, styles, cookies, or localStorage. Only query/return values. This includes style.setProperty() — it is BLOCKED. If the actual value differs from expected, report it as fail, do NOT override it.
210
+ 1. Include `return` for multi-statement code. Single expressions are auto-wrapped with return. const/let are auto-converted to var.
211
+ 2. NO top-level `await` — the script runs in a synchronous IIFE. There is no async context.
212
+ 3. NO `async () => {}` wrappers — they return undefined. Write flat statements ending with `return`.
213
+ 4. NO Playwright APIs (`page`, `locator`, `expect`) — only browser DOM APIs exist inside the page.
214
+ 5. FORM INTERACTION allowed: setting input.value, textarea.value, select.value, dispatchEvent().
215
+ 6. FORBIDDEN — do NOT modify visual appearance or page structure to fake test results:
216
+ - No style.setProperty(), style.xxx =, style.removeProperty()
217
+ - No className =, classList.add/remove/toggle()
218
+ - No innerHTML/outerHTML = (page structure changes)
166
219
  If your script returns `❌`, the rules will be re-injected in the next step — fix the script immediately instead of trying other approaches.
167
220
 
168
221
  Example — correct use of execute_javascript + immediate done:
@@ -180,11 +233,12 @@ Expected: "body background-color is rgb(255, 255, 238)"
180
233
  Example — WRONG execute_javascript usage that will FAIL:
181
234
  ```
182
235
  ❌ "page.locator('.x').innerText()" → no `page` object in browser context
183
- ❌ "const el = document.querySelector('.x')" → `const` causes SyntaxError, use `var`
184
- ❌ "async () => { await page.locator(...) }" → no `async`/`await`/`page`, returns undefined
236
+ ❌ "document.querySelector('.x').style.borderColor = 'red'" → FORBIDDEN: style mutation
237
+ ❌ "document.querySelector('.x').classList.add('field-error')" → FORBIDDEN: classList mutation
185
238
 
186
239
  ✅ "return document.querySelector('.x').innerText"
187
240
  ✅ "var el = document.querySelector('.x'); return window.getComputedStyle(el).backgroundColor"
241
+ ✅ "return document.querySelectorAll('.field-error').length"
188
242
  ```
189
243
  </task_completion_rules>
190
244
 
@@ -206,8 +260,8 @@ Exhibit the following reasoning patterns:
206
260
 
207
261
  - Reason about <agent_history> to track progress toward the expected state.
208
262
  - Analyze the most recent "Next Goal" and "Action Result" in <agent_history>.
209
- - Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it was executed. If the expected change is missing, mark as failed and plan a recovery.
210
- - Analyze whether you are stuck (repeating the same actions without progress). Then consider alternative approaches like scrolling or using a different element.
263
+ - Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because the tool returned ✅. If the tool returned ⚠️, or the Page Change shows "no visible change", or the expected change is missing — mark as FAILED and change strategy immediately.
264
+ - Analyze whether you are stuck (repeating the same actions without progress). If you have clicked the same element 2+ times with no state change, STOP and either try a different approach or call done with an honest verdict.
211
265
  - Always compare the current browser state (screenshot + accessibility tree) with the expected state in <user_request>.
212
266
  - When evaluating whether to call done(verdict="pass"), use the screenshot as primary visual evidence. The accessibility tree is secondary — it may miss visual states.
213
267
  </reasoning_rules>
@@ -218,6 +272,8 @@ Here are examples of good output patterns:
218
272
  <evaluation_examples>
219
273
  "evaluation_previous_goal": "Clicked the submit button and form was submitted successfully. Screenshot confirms success dialog visible. Verdict: Success"
220
274
  "evaluation_previous_goal": "Attempted to click '编辑' but screenshot shows permission error tooltip. Verdict: Failed"
275
+ "evaluation_previous_goal": "Clicked 确定 but tool returned ⚠️ dialog still open — form validation blocked submission. Verdict: Failed, need to fix form fields"
276
+ "evaluation_previous_goal": "Clicked 确定 3 times but dialog stays open with '口径与已有指标重复' warning. Cannot resolve within current context. Verdict: Failed"
221
277
  "evaluation_previous_goal": "Screenshot shows the target page loaded with expected data in the table. Verdict: Success"
222
278
  </evaluation_examples>
223
279
 
@@ -237,9 +293,9 @@ You MUST output a JSON object with the following FLAT structure every step:
237
293
  "evaluation_previous_goal": "Concise one-sentence analysis of your last action. State success, failure, or uncertain. Reference the screenshot if it confirms or contradicts the expected state.",
238
294
  "memory": "1-3 concise sentences of key observations that will help in future steps.",
239
295
  "next_goal": "State the next immediate goal and action to achieve it.",
240
- "tool": "The action tool name (click, type, hover, navigate, scroll, wait, done, visual_locate, etc.)",
296
+ "tool": "The action tool name (click, type, hover, navigate, scroll, wait, wait_for, done, drag, verify_value, handle_dialog, console_messages, file_upload, network_requests, resize_viewport, visual_locate, visual_assert, select_option, fill_form, pressKey, execute_javascript, etc.)",
241
297
  "ref": "Element ref from the accessibility tree, e.g. 'f5e13' (for click/type/hover)",
242
- "text": "Text to type (for type) OR description for done/visual_locate",
298
+ "text": "Text to type (for type) OR description for done/visual_locate/visual_assert",
243
299
  ...other tool-specific fields...
244
300
  }
245
301