explorbot 0.4.9 → 0.5.0
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.
- package/README.md +4 -1
- package/bin/mdq.ts +18 -0
- package/boat/api-tester/src/ai/chief.ts +72 -0
- package/boat/api-tester/src/api-client.ts +37 -0
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/mdq.js +19 -0
- package/dist/boat/api-tester/src/ai/chief.js +69 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +3 -2
- package/dist/src/action.js +4 -2
- package/dist/src/ai/agent.d.ts +3 -1
- package/dist/src/ai/judge-provider.d.ts +17 -0
- package/dist/src/ai/judge-provider.js +56 -0
- package/dist/src/ai/judge-tool.d.ts +2 -0
- package/dist/src/ai/judge-tool.js +33 -0
- package/dist/src/ai/judge.d.ts +28 -0
- package/dist/src/ai/judge.js +71 -0
- package/dist/src/ai/navigator.d.ts +3 -1
- package/dist/src/ai/navigator.js +24 -28
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +67 -9
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +52 -7
- package/dist/src/ai/rerunner.js +7 -0
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/researcher/locators.js +2 -2
- package/dist/src/ai/researcher/pagination.js +1 -1
- package/dist/src/ai/researcher/research-result.js +2 -2
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/task-agent.d.ts +2 -0
- package/dist/src/ai/task-agent.js +3 -1
- package/dist/src/ai/tester.js +19 -15
- package/dist/src/ai/tools.d.ts +4 -3
- package/dist/src/ai/tools.js +35 -7
- package/dist/src/api/request-result.js +2 -1
- package/dist/src/api/xhr-capture.js +2 -1
- package/dist/src/command-handler.d.ts +1 -0
- package/dist/src/command-handler.js +24 -3
- package/dist/src/commands/base-command.d.ts +5 -0
- package/dist/src/commands/base-command.js +3 -0
- package/dist/src/commands/explore-command.d.ts +2 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/freesail-command.js +8 -2
- package/dist/src/commands/init-command.js +1 -1
- package/dist/src/commands/navigate-command.d.ts +2 -1
- package/dist/src/commands/navigate-command.js +6 -0
- package/dist/src/commands/plan-load-command.d.ts +2 -1
- package/dist/src/commands/plan-load-command.js +4 -0
- package/dist/src/commands/plans-command.d.ts +3 -9
- package/dist/src/commands/plans-command.js +11 -21
- package/dist/src/commands/rerun-command.d.ts +2 -1
- package/dist/src/commands/rerun-command.js +5 -1
- package/dist/src/commands/research-command.d.ts +2 -1
- package/dist/src/commands/research-command.js +6 -0
- package/dist/src/commands/test-command.d.ts +2 -1
- package/dist/src/commands/test-command.js +4 -1
- package/dist/src/components/Autocomplete.js +26 -12
- package/dist/src/components/InputReadline.js +10 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/experience-tracker.js +4 -3
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +8 -0
- package/dist/src/explorer.js +2 -3
- package/dist/src/knowledge-tracker.js +1 -1
- package/dist/src/reporter.js +8 -4
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +16 -0
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +54 -2
- package/dist/src/utils/aria-ref.js +1 -1
- package/dist/src/utils/logger.js +9 -3
- package/dist/src/utils/markdown-query.d.ts +1 -48
- package/dist/src/utils/markdown-query.js +1 -444
- package/dist/src/utils/mdq/cli.d.ts +6 -0
- package/dist/src/utils/mdq/cli.js +122 -0
- package/dist/src/utils/mdq/edit.d.ts +24 -0
- package/dist/src/utils/mdq/edit.js +147 -0
- package/dist/src/utils/mdq/query.d.ts +118 -0
- package/dist/src/utils/mdq/query.js +451 -0
- package/dist/src/utils/step-analyzer.d.ts +3 -0
- package/dist/src/utils/step-analyzer.js +7 -0
- package/dist/src/utils/strings.d.ts +1 -0
- package/dist/src/utils/strings.js +7 -0
- package/dist/src/utils/test-files.d.ts +1 -0
- package/dist/src/utils/test-files.js +5 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +7 -0
- package/docs/api-testing/planning.md +1 -1
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
- package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
- package/package.json +3 -2
- package/src/action.ts +4 -2
- package/src/ai/agent.ts +3 -1
- package/src/ai/judge-provider.ts +62 -0
- package/src/ai/judge-tool.ts +35 -0
- package/src/ai/judge.ts +75 -0
- package/src/ai/navigator.ts +24 -27
- package/src/ai/pilot.ts +68 -9
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +51 -7
- package/src/ai/rerunner.ts +4 -0
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/pagination.ts +1 -1
- package/src/ai/researcher/research-result.ts +2 -2
- package/src/ai/researcher.ts +1 -1
- package/src/ai/task-agent.ts +4 -1
- package/src/ai/tester.ts +19 -16
- package/src/ai/tools.ts +42 -7
- package/src/api/request-result.ts +2 -1
- package/src/api/xhr-capture.ts +2 -1
- package/src/command-handler.ts +28 -3
- package/src/commands/base-command.ts +9 -0
- package/src/commands/explore-command.ts +15 -2
- package/src/commands/freesail-command.ts +9 -2
- package/src/commands/init-command.ts +1 -1
- package/src/commands/navigate-command.ts +8 -1
- package/src/commands/plan-load-command.ts +6 -1
- package/src/commands/plans-command.ts +13 -29
- package/src/commands/rerun-command.ts +7 -2
- package/src/commands/research-command.ts +8 -1
- package/src/commands/test-command.ts +6 -2
- package/src/components/Autocomplete.tsx +39 -10
- package/src/components/InputReadline.tsx +10 -1
- package/src/config.ts +1 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/explorer.ts +2 -2
- package/src/knowledge-tracker.ts +1 -1
- package/src/reporter.ts +8 -4
- package/src/state-manager.ts +16 -0
- package/src/test-plan.ts +67 -2
- package/src/utils/aria-ref.ts +1 -1
- package/src/utils/logger.ts +7 -2
- package/src/utils/markdown-query.ts +1 -519
- package/src/utils/mdq/cli.ts +118 -0
- package/src/utils/mdq/edit.ts +158 -0
- package/src/utils/mdq/query.ts +556 -0
- package/src/utils/step-analyzer.ts +8 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
- package/src/utils/url-matcher.ts +7 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Judge — a decision model tier — Design
|
|
2
|
+
|
|
3
|
+
An optional *decision model* answers one narrow question with a probability over the answers offered. Explorbot uses it where it would otherwise guess: a code call site asks, and a confident answer lets the site skip its own, more expensive decision. The model is TypeSafe's Jev (a "System One" model), reached through OpenRouter or TypeSafe's own API.
|
|
4
|
+
|
|
5
|
+
With `ai.decisionModel` unset, nothing registers and nothing calls out.
|
|
6
|
+
|
|
7
|
+
## The interface
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
judge.decide(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>
|
|
11
|
+
|
|
12
|
+
class Decision {
|
|
13
|
+
readonly value: string | null; // the winning option; 'yes' for an approved yes/no
|
|
14
|
+
readonly confidence: number; // P(yes) for a yes/no, the chosen option's probability for a list
|
|
15
|
+
get approved(): boolean; // value !== null
|
|
16
|
+
get rejected(): boolean; // value === null
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **An array is a categorization**, sent as a Choice. `value` is the chosen option. Including `UNDECIDED` lets the model say none fits.
|
|
21
|
+
- **A boolean or `null` is a yes/no**, sent as a Noul, whose single number is P(yes). Only a confident yes approves.
|
|
22
|
+
- **Approved means the answer's probability is above 70%** and it isn't `UNDECIDED`.
|
|
23
|
+
- **Everything else is rejected**: a confident no, a low probability, `UNDECIDED`, a timeout, a failed request, a list with fewer than two options, or the direct path being disabled. `decide` never throws and never returns `null`.
|
|
24
|
+
|
|
25
|
+
The threshold and every failure mode live in one place. A call site only ever sees a confident decision or a rejection, so it cannot misread uncertainty.
|
|
26
|
+
|
|
27
|
+
### The one rule for call sites
|
|
28
|
+
|
|
29
|
+
**`rejected` means "not approved", never "confidently no".** It absorbs uncertainty, so a question must be phrased so that `approved` is the action the site would take, and `rejected` falls through to today's behaviour. Then `if (decision?.approved)` is correct by construction.
|
|
30
|
+
|
|
31
|
+
`consult(...)` is the same call without the direct-path gate, used by the tool.
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
ai: { decisionModel: { provider: 'openrouter', model: 'typesafe/jev-1.13' } } // via OpenRouter
|
|
37
|
+
ai: { decisionModel: { provider: 'typesafe', model: 'jev-latest' } } // TypeSafe API directly
|
|
38
|
+
ai: { decisionModel: { provider: 'openrouter', model: 'typesafe/jev-1.13', tool: false } } // direct sites only
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`config.ts` holds only the field type. An unknown provider or a missing API key throws at startup, naming what to fix.
|
|
42
|
+
|
|
43
|
+
Everything transport-specific lives in `src/ai/judge-provider.ts`: `endpointFor()` (a `switch` over `openrouter` and `typesafe`, each with its endpoint and API-key variable), the HTTP call, timeout, and the Noul/Choice wire format. That file is temporary: when the Vercel AI SDK supports decision models it is deleted and `decisionModel` becomes a regular provider-built model.
|
|
44
|
+
|
|
45
|
+
`Judge.fromConfig()` builds the judge; it reaches agents through `AgentDeps` and tools through `ToolDeps`.
|
|
46
|
+
|
|
47
|
+
## Where it is used
|
|
48
|
+
|
|
49
|
+
| Site | Question | Approved means |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `failedToolResult`, multi-element branch | Which listed element does the intent name? | suggest that element by `elementIndex` |
|
|
52
|
+
| `Pilot.analyzeProgress` | The run is moving toward the goal and can continue without a supervisor now. | skip the review silently |
|
|
53
|
+
| `Pilot.settleExpectations`, text-only path | What did this run establish about the expected outcome? | settle it; the rest go to the agentic model |
|
|
54
|
+
| `Navigator.verifyState`, before the prompt | Which already verified claim means the same as this one? | treat it as verified, skip the HTML-bearing prompt |
|
|
55
|
+
| `Navigator.verifyState`, inexpressible branch | The page shows that this claim is true. | report it as a judgement, not an assertion |
|
|
56
|
+
| Prima `go()`, semantic target | Which listed control leads to the target? / The page now shows the target. | click the ref, confirm arrival, skip the navigator |
|
|
57
|
+
| `judge` tool (Tester and Pilot) | whatever the model asks | the answer; otherwise "not confirmed" |
|
|
58
|
+
|
|
59
|
+
Invariants the sites keep:
|
|
60
|
+
|
|
61
|
+
- **A judge answer never enters `verifications`.** A dedup match skips the work but writes no cache entry for the new claim, because a cache entry stands in for a proof.
|
|
62
|
+
- **Prima confirms arrival** before returning a success envelope. `cli.ts` exits on `envelope.ok`.
|
|
63
|
+
- **The tool assembles its own state**: scenario, compact ARIA capped at `JUDGE_PAGE_CAP`, and recent steps. The model supplies only the question.
|
|
64
|
+
|
|
65
|
+
## Deliberately not used
|
|
66
|
+
|
|
67
|
+
- **Pilot's verdict.** A generic "the app never held the state the scenario assumes" would fire on boundary give-ups, like "previous page" on page 1 when `»` is listed, and turn an executable scenario into a clean-looking skip. That hides the execution gap. The fix for a boundary give-up belongs in planning, not the verdict.
|
|
68
|
+
- **Experience filtering.** It removes no model call, and one question per stored block on every prompt build costs more than it saves.
|
|
69
|
+
- **Generation, screenshots, tool-calling loops.** The model is text-only and does not generate.
|
|
70
|
+
|
|
71
|
+
## Measurements behind the threshold
|
|
72
|
+
|
|
73
|
+
On states reconstructed from recorded traces:
|
|
74
|
+
|
|
75
|
+
- A literal, concrete phrasing beat an abstract one on identical state: 0.88 versus 0.58. Questions should name what they expect to see.
|
|
76
|
+
- A composite "continue or call Pilot" Choice tied at confidence 0.08 on a run that was progressing. Under `rejected` semantics, a tie falls through to a review, which is the safe direction.
|
|
77
|
+
- The first live probe picked `copy` over `root` at 0.48 versus 0.41 on a dialog whose Copy button was disabled. A 70% bar rejects that, correctly.
|
|
78
|
+
|
|
79
|
+
Threshold and phrasing should be tuned from traces: every call records `question`, `value` and `confidence` on its `judge.decide` span, and failures record their reason.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"check": "biome check .",
|
|
61
61
|
"check:fix": "biome check --write .",
|
|
62
62
|
"langfuse:export": "bun run .claude/skills/explorbot-debug/langfuse-export.ts",
|
|
63
|
-
"build:prima": "bun run scripts/build-prima-npm.ts"
|
|
63
|
+
"build:prima": "bun run scripts/build-prima-npm.ts",
|
|
64
|
+
"build:mdq": "bun run scripts/build-mdq-npm.ts"
|
|
64
65
|
},
|
|
65
66
|
"keywords": [
|
|
66
67
|
"cli",
|
package/src/action.ts
CHANGED
|
@@ -16,7 +16,9 @@ import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
|
16
16
|
import { Overlay, OverlayPage } from './utils/overlay.js';
|
|
17
17
|
import { sleep, waitForPageReadiness } from './utils/page-readiness.ts';
|
|
18
18
|
import type { Region } from './utils/region.js';
|
|
19
|
+
import { isInternalStep } from './utils/step-analyzer.ts';
|
|
19
20
|
import { safeFilename } from './utils/strings.ts';
|
|
21
|
+
import { isSameHostFamily } from './utils/url-matcher.js';
|
|
20
22
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from './utils/web-sandbox.ts';
|
|
21
23
|
|
|
22
24
|
const debugLog = createDebug('explorbot:action');
|
|
@@ -317,7 +319,7 @@ class Action {
|
|
|
317
319
|
|
|
318
320
|
const url = URL.parse(request.url());
|
|
319
321
|
if (!url) return;
|
|
320
|
-
if (url.
|
|
322
|
+
if (!isSameHostFamily(url.href, this.baseOrigin)) return;
|
|
321
323
|
|
|
322
324
|
const call: NetworkCall = { method: request.method(), path: url.pathname, status };
|
|
323
325
|
if (this.networkRequests.some((r) => r.method === call.method && r.path === call.path && r.status === call.status)) return;
|
|
@@ -563,7 +565,7 @@ export const attachStepLogger = (target: ExecutedStep[], assertionsTarget?: Arra
|
|
|
563
565
|
let batchFailed = false;
|
|
564
566
|
const listener: StepListener = (step, error) => {
|
|
565
567
|
if (!step?.toCode) return;
|
|
566
|
-
if (step
|
|
568
|
+
if (isInternalStep(step)) return;
|
|
567
569
|
|
|
568
570
|
const existing = recorded.get(step);
|
|
569
571
|
if (existing) {
|
package/src/ai/agent.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type Explorer from '../explorer.ts';
|
|
|
4
4
|
import type { KnowledgeTracker } from '../knowledge-tracker.ts';
|
|
5
5
|
import type { PlaywrightRecorder } from '../playwright-recorder.ts';
|
|
6
6
|
import type { StateManager } from '../state-manager.ts';
|
|
7
|
+
import type { Judge } from './judge.ts';
|
|
7
8
|
import type { AIProvider } from './provider.ts';
|
|
8
9
|
|
|
9
10
|
export interface Agent {
|
|
@@ -18,6 +19,7 @@ export interface AgentDeps {
|
|
|
18
19
|
knowledgeTracker: KnowledgeTracker;
|
|
19
20
|
requestStore: RequestStore;
|
|
20
21
|
playwrightRecorder: PlaywrightRecorder;
|
|
22
|
+
judge?: Judge;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export type ToolDeps = Pick<AgentDeps, 'explorer' | 'stateManager' | 'ai'>;
|
|
25
|
+
export type ToolDeps = Pick<AgentDeps, 'explorer' | 'stateManager' | 'ai' | 'judge'>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
2
|
+
|
|
3
|
+
export class JudgeProvider {
|
|
4
|
+
private fetchImpl: typeof fetch = fetch;
|
|
5
|
+
private requestTimeoutMs = REQUEST_TIMEOUT_MS;
|
|
6
|
+
private url: string;
|
|
7
|
+
private apiKey: string;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
provider: string,
|
|
11
|
+
readonly model: string
|
|
12
|
+
) {
|
|
13
|
+
const { url, keyName } = JudgeProvider.endpointFor(provider);
|
|
14
|
+
const apiKey = process.env[keyName];
|
|
15
|
+
if (!apiKey) throw new Error(`Set ${keyName} to use the decision model`);
|
|
16
|
+
this.url = url;
|
|
17
|
+
this.apiKey = apiKey;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async decide(state: unknown, question: string, options?: string[]): Promise<ProviderDecision> {
|
|
21
|
+
let q: Record<string, unknown> = { type: 'noul', instructions: question };
|
|
22
|
+
if (options) q = { type: 'choice', instructions: question, criteria: Object.fromEntries(options.map((option, index) => [String(index), option])) };
|
|
23
|
+
const body = JSON.stringify({ model: this.model, state, questions: { q } });
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
26
|
+
try {
|
|
27
|
+
const response = await this.fetchImpl(this.url, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' },
|
|
30
|
+
body,
|
|
31
|
+
signal: controller.signal,
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) throw new Error(`http_${response.status}`);
|
|
34
|
+
|
|
35
|
+
const answer = (await response.json())?.answers?.q;
|
|
36
|
+
if (!options && typeof answer?.noul === 'number') return { value: 'yes', probability: answer.noul };
|
|
37
|
+
|
|
38
|
+
const value = options?.[Number(answer?.choice)];
|
|
39
|
+
const probability = answer?.probabilities?.[answer?.choice];
|
|
40
|
+
if (value === undefined || typeof probability !== 'number') throw new Error('malformed_body');
|
|
41
|
+
return { value, probability };
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private static endpointFor(provider: string): { url: string; keyName: string } {
|
|
48
|
+
switch (provider) {
|
|
49
|
+
case 'openrouter':
|
|
50
|
+
return { url: 'https://openrouter.ai/api/alpha/decisions', keyName: 'OPENROUTER_API_KEY' };
|
|
51
|
+
case 'typesafe':
|
|
52
|
+
return { url: 'https://api.typesafe.ai/v1/systemone', keyName: 'TYPESAFE_API_KEY' };
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unknown decision model provider "${provider}" — use "openrouter" or "typesafe"`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ProviderDecision {
|
|
60
|
+
value: string;
|
|
61
|
+
probability: number;
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import dedent from 'dedent';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import type { ToolDeps } from './agent.ts';
|
|
5
|
+
import { failedToolResult, successToolResult } from './tools.ts';
|
|
6
|
+
|
|
7
|
+
export function createJudgeTool(deps: ToolDeps, buildState: () => Promise<Record<string, unknown>>): Record<string, any> {
|
|
8
|
+
const judge = deps.judge;
|
|
9
|
+
if (!judge?.toolEnabled) return {};
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
judge: tool({
|
|
13
|
+
description: dedent`
|
|
14
|
+
Settle one judgement about the current page instead of guessing. Phrase it literally and concretely.
|
|
15
|
+
`,
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
question: z.string().describe('The statement to confirm, or the question the options answer'),
|
|
18
|
+
options: z.array(z.string()).optional().describe('Possible answers. Omit to confirm a statement'),
|
|
19
|
+
context: z.string().optional().describe('Anything the page observation does not already carry'),
|
|
20
|
+
}),
|
|
21
|
+
execute: async ({ question, options, context }) => {
|
|
22
|
+
const state = await buildState();
|
|
23
|
+
if (context) state.context = context;
|
|
24
|
+
|
|
25
|
+
const decision = await judge.consult(question, options ?? null, state);
|
|
26
|
+
if (decision.rejected) {
|
|
27
|
+
return failedToolResult('judge', `Not confirmed: ${question}`, {
|
|
28
|
+
suggestion: 'The page does not settle this. Gather more context or take another route; do not assume either answer.',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return successToolResult('judge', { question, answer: decision.value, confidence: decision.confidence });
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/ai/judge.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { clearActivity, setActivity } from '../activity.ts';
|
|
2
|
+
import type { AIConfig } from '../config.ts';
|
|
3
|
+
import { Observability } from '../observability.ts';
|
|
4
|
+
import { createDebug } from '../utils/logger.ts';
|
|
5
|
+
import { JudgeProvider } from './judge-provider.ts';
|
|
6
|
+
|
|
7
|
+
const debugLog = createDebug('explorbot:judge');
|
|
8
|
+
|
|
9
|
+
const APPROVAL_THRESHOLD = 0.7;
|
|
10
|
+
|
|
11
|
+
export const UNDECIDED = 'undecided';
|
|
12
|
+
export const JUDGE_PAGE_CAP = 12000;
|
|
13
|
+
|
|
14
|
+
export class Decision {
|
|
15
|
+
constructor(
|
|
16
|
+
readonly value: string | null,
|
|
17
|
+
readonly confidence: number
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
get approved(): boolean {
|
|
21
|
+
return this.value !== null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get rejected(): boolean {
|
|
25
|
+
return this.value === null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Judge {
|
|
30
|
+
constructor(
|
|
31
|
+
private provider: JudgeProvider,
|
|
32
|
+
private enabled: { tool: boolean; direct: boolean }
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
static fromConfig(config: AIConfig['decisionModel']): Judge | null {
|
|
36
|
+
if (!config) return null;
|
|
37
|
+
return new Judge(new JudgeProvider(config.provider, config.model), { tool: config.tool !== false, direct: config.direct !== false });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get toolEnabled(): boolean {
|
|
41
|
+
return this.enabled.tool;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async decide(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision> {
|
|
45
|
+
if (!this.enabled.direct) return new Decision(null, 0);
|
|
46
|
+
return this.consult(question, options, state);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async consult(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision> {
|
|
50
|
+
if (Array.isArray(options) && options.length < 2) return new Decision(null, 0);
|
|
51
|
+
return Observability.run('judge.decide', { tags: ['judge'] }, async () => {
|
|
52
|
+
setActivity('⚖️ Asking judge...', 'ai');
|
|
53
|
+
const decision = await this.request(question, options, state).finally(() => clearActivity());
|
|
54
|
+
Observability.getSpan()?.setAttribute('ai.telemetry.metadata.judgeDecision', JSON.stringify({ question, value: decision.value, confidence: decision.confidence }));
|
|
55
|
+
return decision;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async request(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision> {
|
|
60
|
+
let list: string[] | undefined;
|
|
61
|
+
if (Array.isArray(options)) list = options;
|
|
62
|
+
|
|
63
|
+
const answer = await this.provider.decide(state, question, list).catch((error: unknown) => this.recordFailure(error));
|
|
64
|
+
if (!answer) return new Decision(null, 0);
|
|
65
|
+
if (answer.probability <= APPROVAL_THRESHOLD) return new Decision(null, answer.probability);
|
|
66
|
+
if (answer.value === UNDECIDED) return new Decision(null, answer.probability);
|
|
67
|
+
return new Decision(answer.value, answer.probability);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private recordFailure(error: unknown): null {
|
|
71
|
+
debugLog('judge declined: %s', error);
|
|
72
|
+
Observability.getSpan()?.setAttribute('ai.telemetry.metadata.judgeError', String(error));
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/ai/navigator.ts
CHANGED
|
@@ -18,9 +18,10 @@ import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
|
18
18
|
import { loop, pause } from '../utils/loop.js';
|
|
19
19
|
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
20
20
|
import { normalizeInlineText } from '../utils/strings.ts';
|
|
21
|
-
import { extractStatePath, matchesNavigationUrl } from '../utils/url-matcher.js';
|
|
21
|
+
import { extractStatePath, isSameHostFamily, matchesNavigationUrl } from '../utils/url-matcher.js';
|
|
22
22
|
import type { Agent, AgentDeps } from './agent.js';
|
|
23
23
|
import type { Conversation } from './conversation.js';
|
|
24
|
+
import { type Decision, JUDGE_PAGE_CAP, type Judge, UNDECIDED } from './judge.ts';
|
|
24
25
|
import type { Provider } from './provider.js';
|
|
25
26
|
import { Researcher } from './researcher.ts';
|
|
26
27
|
import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
|
|
@@ -80,6 +81,7 @@ class Navigator implements Agent {
|
|
|
80
81
|
private explorer: Explorer;
|
|
81
82
|
private config: ExplorbotConfig;
|
|
82
83
|
private stateManager: StateManager;
|
|
84
|
+
private judge?: Judge;
|
|
83
85
|
|
|
84
86
|
constructor(deps: AgentDeps) {
|
|
85
87
|
this.provider = deps.ai;
|
|
@@ -89,6 +91,7 @@ class Navigator implements Agent {
|
|
|
89
91
|
this.knowledgeTracker = deps.knowledgeTracker;
|
|
90
92
|
this.experienceTracker = deps.stateManager.getExperienceTracker();
|
|
91
93
|
this.hooksRunner = new HooksRunner(deps.explorer, deps.config);
|
|
94
|
+
this.judge = deps.judge;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
private get verifyAttempts(): number {
|
|
@@ -99,15 +102,6 @@ class Navigator implements Agent {
|
|
|
99
102
|
return this.config.ai?.agents?.navigator?.verifyTimeout ?? 1500;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
private getBaseOrigin(): string | null {
|
|
103
|
-
const baseUrl = this.config.playwright.url;
|
|
104
|
-
try {
|
|
105
|
-
return new URL(baseUrl).origin;
|
|
106
|
-
} catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
105
|
private getComparableCurrentUrl(stateManager: any, expectedUrl: string): string {
|
|
112
106
|
const currentState = stateManager.getCurrentState();
|
|
113
107
|
if (!currentState) return '';
|
|
@@ -126,18 +120,12 @@ class Navigator implements Agent {
|
|
|
126
120
|
const currentFullUrl = currentState.fullUrl || currentState.url || '';
|
|
127
121
|
if (!currentFullUrl) return false;
|
|
128
122
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (/^https?:\/\//i.test(expectedUrl)) {
|
|
132
|
-
return currentOrigin === new URL(expectedUrl).origin;
|
|
133
|
-
}
|
|
123
|
+
if (!/^https?:\/\//i.test(currentFullUrl)) return !/^https?:\/\//i.test(expectedUrl);
|
|
124
|
+
if (/^https?:\/\//i.test(expectedUrl)) return isSameHostFamily(currentFullUrl, expectedUrl);
|
|
134
125
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} catch {
|
|
139
|
-
return !/^https?:\/\//i.test(expectedUrl);
|
|
140
|
-
}
|
|
126
|
+
const baseUrl = this.config.playwright.url;
|
|
127
|
+
if (!baseUrl) return true;
|
|
128
|
+
return isSameHostFamily(currentFullUrl, baseUrl);
|
|
141
129
|
}
|
|
142
130
|
|
|
143
131
|
private isOnExpectedPage(expectedUrl: string, stateManager: any): boolean {
|
|
@@ -325,8 +313,9 @@ class Navigator implements Agent {
|
|
|
325
313
|
lastFailure = `Reached ${check.freshState.url} but the page state did not change`;
|
|
326
314
|
tag('warning').log(`Page state did not change at ${check.freshState.url}`);
|
|
327
315
|
} else {
|
|
328
|
-
|
|
329
|
-
|
|
316
|
+
const reachedUrl = check.freshState.fullUrl || check.freshState.url;
|
|
317
|
+
lastFailure = `Reached ${reachedUrl}, expected ${expectedUrl}`;
|
|
318
|
+
tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${reachedUrl}`);
|
|
330
319
|
}
|
|
331
320
|
batchFailures.push({
|
|
332
321
|
code: codeBlock,
|
|
@@ -584,7 +573,7 @@ class Navigator implements Agent {
|
|
|
584
573
|
const countVisit = (value?: string | null) => {
|
|
585
574
|
if (!value) return;
|
|
586
575
|
const normalized = normalizeUrl(value);
|
|
587
|
-
|
|
576
|
+
visitCounts.set(normalized, (visitCounts.get(normalized) || 0) + 1);
|
|
588
577
|
};
|
|
589
578
|
|
|
590
579
|
for (const transition of history) {
|
|
@@ -595,7 +584,7 @@ class Navigator implements Agent {
|
|
|
595
584
|
if (opts?.visitedUrls) {
|
|
596
585
|
for (const url of opts.visitedUrls) {
|
|
597
586
|
const normalized = normalizeUrl(url);
|
|
598
|
-
if (
|
|
587
|
+
if (!visitCounts.has(normalized)) {
|
|
599
588
|
visitCounts.set(normalized, 1);
|
|
600
589
|
}
|
|
601
590
|
}
|
|
@@ -696,7 +685,7 @@ class Navigator implements Agent {
|
|
|
696
685
|
return suggestion;
|
|
697
686
|
}
|
|
698
687
|
|
|
699
|
-
async verifyState(message: string, actionResult: ActionResult): Promise<{ verified: boolean; inexpressible: boolean; results: AssertionResult[]; successfulCodes: string[]; assertionSteps: Array<{ name: string; args: any[] }>; totalAttempted: number }> {
|
|
688
|
+
async verifyState(message: string, actionResult: ActionResult): Promise<{ verified: boolean; inexpressible: boolean; results: AssertionResult[]; successfulCodes: string[]; assertionSteps: Array<{ name: string; args: any[] }>; totalAttempted: number; judged?: Decision }> {
|
|
700
689
|
tag('info').log('AI Navigator verifying state at', actionResult.url);
|
|
701
690
|
debugLog('Verification message:', message);
|
|
702
691
|
|
|
@@ -706,6 +695,13 @@ class Navigator implements Agent {
|
|
|
706
695
|
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
707
696
|
}
|
|
708
697
|
|
|
698
|
+
const verifiedClaims = Object.keys(actionResult.verifications ?? {}).filter((claim) => actionResult.getVerification(claim) === true);
|
|
699
|
+
const same = await this.judge?.decide('Which already verified claim means the same as the claim under consideration?', [...verifiedClaims, UNDECIDED], { claim: message });
|
|
700
|
+
if (same?.approved) {
|
|
701
|
+
tag('operation').log(`Judge matched claim to an already verified one: "${same.value}"`);
|
|
702
|
+
return { verified: true, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
703
|
+
}
|
|
704
|
+
|
|
709
705
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
710
706
|
let experience = '';
|
|
711
707
|
|
|
@@ -846,7 +842,8 @@ class Navigator implements Agent {
|
|
|
846
842
|
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
847
843
|
if (inexpressible) {
|
|
848
844
|
tag('warning').log('No assertion could express this claim');
|
|
849
|
-
|
|
845
|
+
const judged = await this.judge?.decide('The page shows that this claim is true.', null, { claim: message, page: actionResult.getCompactARIA().slice(0, JUDGE_PAGE_CAP) });
|
|
846
|
+
return { verified: false, inexpressible, results, successfulCodes, assertionSteps, totalAttempted, judged };
|
|
850
847
|
}
|
|
851
848
|
|
|
852
849
|
actionResult.addVerification(message, verified);
|
package/src/ai/pilot.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type Explorer from '../explorer.ts';
|
|
|
8
8
|
import type { PlaywrightRecorder } from '../playwright-recorder.ts';
|
|
9
9
|
import type { StateManager } from '../state-manager.ts';
|
|
10
10
|
import { Stats } from '../stats.ts';
|
|
11
|
-
import { type Test, TestResult } from '../test-plan.ts';
|
|
11
|
+
import { type Test, TestResult, TestStatus } from '../test-plan.ts';
|
|
12
12
|
import { collectInteractiveNodes } from '../utils/aria.ts';
|
|
13
13
|
import { ErrorPageError } from '../utils/error-page.ts';
|
|
14
14
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
@@ -19,6 +19,7 @@ import type { Agent, AgentDeps } from './agent.ts';
|
|
|
19
19
|
import type { Conversation } from './conversation.ts';
|
|
20
20
|
import type { Fisherman } from './fisherman.ts';
|
|
21
21
|
import { createAskApiTool } from './fisherman/tools.ts';
|
|
22
|
+
import { type Judge, UNDECIDED } from './judge.ts';
|
|
22
23
|
import type { Navigator } from './navigator.ts';
|
|
23
24
|
import type { Provider } from './provider.ts';
|
|
24
25
|
import type { Researcher } from './researcher.ts';
|
|
@@ -33,6 +34,10 @@ const PILOT_REASONING_LIMIT = 500;
|
|
|
33
34
|
const PILOT_MESSAGE_LIMIT = 2;
|
|
34
35
|
const PILOT_MESSAGE_MAX_LENGTH = 160;
|
|
35
36
|
const PILOT_REQUEST_LIMIT = 5;
|
|
37
|
+
const OUTCOME_STATUS: Record<string, SettledStatus> = {
|
|
38
|
+
'The run shows this outcome happened.': 'passed',
|
|
39
|
+
'The run shows this outcome did not happen.': 'failed',
|
|
40
|
+
};
|
|
36
41
|
|
|
37
42
|
export class Pilot implements Agent {
|
|
38
43
|
emoji = '🧭';
|
|
@@ -45,6 +50,7 @@ export class Pilot implements Agent {
|
|
|
45
50
|
private requestStore: RequestStore;
|
|
46
51
|
private playwrightRecorder: PlaywrightRecorder;
|
|
47
52
|
private fisherman: Fisherman | null = null;
|
|
53
|
+
private judge?: Judge;
|
|
48
54
|
|
|
49
55
|
constructor(deps: AgentDeps, agentTools: any, researcher: Researcher) {
|
|
50
56
|
this.provider = deps.ai;
|
|
@@ -54,6 +60,7 @@ export class Pilot implements Agent {
|
|
|
54
60
|
this.stateManager = deps.stateManager;
|
|
55
61
|
this.requestStore = deps.requestStore;
|
|
56
62
|
this.playwrightRecorder = deps.playwrightRecorder;
|
|
63
|
+
this.judge = deps.judge;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
setFisherman(fisherman: Fisherman): void {
|
|
@@ -118,7 +125,9 @@ export class Pilot implements Agent {
|
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
const schema = z.object({
|
|
121
|
-
decision: z
|
|
128
|
+
decision: z
|
|
129
|
+
.enum(['pass', 'fail', 'continue', 'skipped'])
|
|
130
|
+
.describe('pass = scenario goal accomplished, fail = the app misbehaved, continue = tester should keep going, skipped = the scenario cannot be judged against this app (its premise does not hold, it is irrelevant, or systematic execution failures prevented testing)'),
|
|
122
131
|
reason: z.string().describe('Concise user-facing reason, maximum 1 short sentence and 120 characters. Do NOT repeat the decision status; explain only the evidence. For continue: explain why rejected and suggest alternatives.'),
|
|
123
132
|
guidance: z.string().nullable().describe('Required for "continue": specific actionable instruction for the tester — what exactly to verify, retry differently, or complete next. Be concrete.'),
|
|
124
133
|
requestVerification: z
|
|
@@ -407,9 +416,13 @@ export class Pilot implements Agent {
|
|
|
407
416
|
DOM assertion can't be made.
|
|
408
417
|
Do not pass when Tester achieved only a related navigation/filter/tab/status outcome instead of the
|
|
409
418
|
requested action, workflow, or entity detail goal.
|
|
410
|
-
- "fail":
|
|
411
|
-
|
|
412
|
-
|
|
419
|
+
- "fail": the app misbehaved — the scenario's action ran against the right target and the app
|
|
420
|
+
produced a wrong, broken, or missing outcome. Not reaching the goal is not by itself a fail.
|
|
421
|
+
- "skipped": the scenario cannot be judged against this app — the page shows its premise does not
|
|
422
|
+
hold (the assumed constraint, field, or behaviour is designed differently), the target entity or
|
|
423
|
+
feature is not the one here, the scenario is irrelevant, OR systematic infrastructure failures
|
|
424
|
+
(LLM errors, crashes) prevented testing. NOT for "test failed to interact" — that's "fail" or
|
|
425
|
+
"continue".
|
|
413
426
|
- "continue": goal incomplete but the control for the NEXT step is present on the current page, or a
|
|
414
427
|
concrete missing check would change your verdict. Guidance must name that step.
|
|
415
428
|
If a verify() asserted a state that was ALREADY TRUE before the test, it proves nothing — reject.
|
|
@@ -542,6 +555,9 @@ export class Pilot implements Agent {
|
|
|
542
555
|
const actionsContext = this.formatActions(toolCalls);
|
|
543
556
|
const stateContext = this.buildStateContext(currentState);
|
|
544
557
|
|
|
558
|
+
const healthy = await this.judge?.decide('The run is moving toward the goal and can continue without a supervisor reviewing it now.', null, { scenario: task.scenario, state: stateContext, recentActions: actionsContext });
|
|
559
|
+
if (healthy?.approved) return '';
|
|
560
|
+
|
|
545
561
|
const hasFailures = toolCalls.length === 0 || toolCalls.some((t) => !t.wasSuccessful);
|
|
546
562
|
|
|
547
563
|
const text = await this.sendToPilot(
|
|
@@ -586,7 +602,11 @@ export class Pilot implements Agent {
|
|
|
586
602
|
|
|
587
603
|
let undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
|
|
588
604
|
if (image) undecided = task.expected;
|
|
589
|
-
|
|
605
|
+
|
|
606
|
+
let settledByJudge = new Map<string, SettledStatus>();
|
|
607
|
+
if (!image) settledByJudge = await this.settleByJudge(task, undecided);
|
|
608
|
+
undecided = undecided.filter((text) => !settledByJudge.has(text));
|
|
609
|
+
if (!undecided.length) return task.expected.map((text) => ({ text, status: settledByJudge.get(text) || decided(text) }));
|
|
590
610
|
|
|
591
611
|
const schema = z.object({
|
|
592
612
|
outcomes: z.array(
|
|
@@ -659,6 +679,8 @@ export class Pilot implements Agent {
|
|
|
659
679
|
|
|
660
680
|
const judged = new Map((response?.object?.outcomes || []).map((outcome: any) => [outcome.expectation, outcome]));
|
|
661
681
|
return task.expected.map((text) => {
|
|
682
|
+
const byJudge = settledByJudge.get(text);
|
|
683
|
+
if (byJudge) return { text, status: byJudge };
|
|
662
684
|
if (!undecided.includes(text)) return { text, status: decided(text) };
|
|
663
685
|
const outcome = judged.get(text) as { status: SettledStatus; evidence?: string } | undefined;
|
|
664
686
|
if (!outcome) return { text, status: 'unverified' as SettledStatus };
|
|
@@ -666,6 +688,22 @@ export class Pilot implements Agent {
|
|
|
666
688
|
});
|
|
667
689
|
}
|
|
668
690
|
|
|
691
|
+
private async settleByJudge(task: Test, expectations: string[]): Promise<Map<string, SettledStatus>> {
|
|
692
|
+
const settled = new Map<string, SettledStatus>();
|
|
693
|
+
const judge = this.judge;
|
|
694
|
+
if (!judge) return settled;
|
|
695
|
+
|
|
696
|
+
const state = { scenario: task.scenario, runLog: task.notesToString() || 'No steps recorded.' };
|
|
697
|
+
await Promise.all(
|
|
698
|
+
expectations.map(async (text) => {
|
|
699
|
+
const decision = await judge.decide(`What did this run establish about the expected outcome: ${text}`, [...Object.keys(OUTCOME_STATUS), UNDECIDED], state);
|
|
700
|
+
const status = OUTCOME_STATUS[decision.value ?? ''];
|
|
701
|
+
if (status) settled.set(text, status);
|
|
702
|
+
})
|
|
703
|
+
);
|
|
704
|
+
return settled;
|
|
705
|
+
}
|
|
706
|
+
|
|
669
707
|
private formatExpectations(task: Test): string {
|
|
670
708
|
const checked = task.getCheckedExpectations();
|
|
671
709
|
const remaining = task.getRemainingExpectations();
|
|
@@ -684,6 +722,7 @@ export class Pilot implements Agent {
|
|
|
684
722
|
this.conversation!.addUserText(finalUserText);
|
|
685
723
|
|
|
686
724
|
const tools = { ...this.pickPlanningTools(), ...this.buildFishermanTools(opts.task) };
|
|
725
|
+
const preparedCount = opts.task.preparedData.length;
|
|
687
726
|
|
|
688
727
|
const result = await this.provider.invokeConversation(this.conversation!, tools, {
|
|
689
728
|
maxToolRoundtrips: opts.maxToolRoundtrips ?? 0,
|
|
@@ -692,7 +731,7 @@ export class Pilot implements Agent {
|
|
|
692
731
|
stopWhen: () => opts.task.hasFinished,
|
|
693
732
|
telemetry: { functionId },
|
|
694
733
|
});
|
|
695
|
-
const text = result?.response?.text || '';
|
|
734
|
+
const text = this.announcePreparedData(result?.response?.text || '', opts.task, preparedCount);
|
|
696
735
|
const learned = (result?.toolExecutions || []).filter((e: any) => e.toolName === 'learnExperience' && e.output?.content).map((e: any) => ({ url: e.output.url, content: e.output.content }));
|
|
697
736
|
if (learned.length === 0) return text;
|
|
698
737
|
opts.task.applyExperience(learned);
|
|
@@ -708,6 +747,24 @@ export class Pilot implements Agent {
|
|
|
708
747
|
`;
|
|
709
748
|
}
|
|
710
749
|
|
|
750
|
+
private announcePreparedData(text: string, task: Test, preparedCount: number): string {
|
|
751
|
+
const prepared = task.preparedData.slice(preparedCount);
|
|
752
|
+
if (prepared.length === 0) return text;
|
|
753
|
+
|
|
754
|
+
let refresh = '';
|
|
755
|
+
if (task.status === TestStatus.IN_PROGRESS) refresh = 'It was created after the page loaded, so the page does not show it yet. Run I.refreshPage() through form() before looking for it.';
|
|
756
|
+
|
|
757
|
+
return dedent`
|
|
758
|
+
${text}
|
|
759
|
+
|
|
760
|
+
<prepared_data>
|
|
761
|
+
Pilot created this data through the API for this test. Use it instead of creating the same data through the UI:
|
|
762
|
+
${prepared.map((item) => `- ${item}`).join('\n')}
|
|
763
|
+
${refresh}
|
|
764
|
+
</prepared_data>
|
|
765
|
+
`;
|
|
766
|
+
}
|
|
767
|
+
|
|
711
768
|
private getExperienceToc(): string {
|
|
712
769
|
const state = this.stateManager.getCurrentState();
|
|
713
770
|
if (!state) return '';
|
|
@@ -715,7 +772,7 @@ export class Pilot implements Agent {
|
|
|
715
772
|
}
|
|
716
773
|
|
|
717
774
|
private pickPlanningTools() {
|
|
718
|
-
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser } = this.agentTools ?? {};
|
|
775
|
+
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser, judge } = this.agentTools ?? {};
|
|
719
776
|
const planning: Record<string, unknown> = {};
|
|
720
777
|
if (see) planning.see = see;
|
|
721
778
|
if (context) planning.context = context;
|
|
@@ -725,6 +782,7 @@ export class Pilot implements Agent {
|
|
|
725
782
|
if (xpathCheck) planning.xpathCheck = xpathCheck;
|
|
726
783
|
if (learnExperience) planning.learnExperience = learnExperience;
|
|
727
784
|
if (askUser) planning.askUser = askUser;
|
|
785
|
+
if (judge) planning.judge = judge;
|
|
728
786
|
withdrawVisionTools(planning);
|
|
729
787
|
return planning;
|
|
730
788
|
}
|
|
@@ -771,6 +829,7 @@ export class Pilot implements Agent {
|
|
|
771
829
|
});
|
|
772
830
|
const stepText = `Precondition: created ${items.join(', ')}`;
|
|
773
831
|
task.addStep(stepText);
|
|
832
|
+
task.preparedData.push(...items);
|
|
774
833
|
tag('success').log(stepText);
|
|
775
834
|
|
|
776
835
|
return { noted: true, prepared: true, created: result.created };
|
|
@@ -1151,7 +1210,7 @@ export class Pilot implements Agent {
|
|
|
1151
1210
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1152
1211
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1153
1212
|
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1154
|
-
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.
|
|
1213
|
+
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.refreshPage() through form.
|
|
1155
1214
|
|
|
1156
1215
|
${capabilityGroundingRule}
|
|
1157
1216
|
|