explorbot 0.4.10 → 0.5.1
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/explorbot-cli.ts +2 -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/README.md +31 -4
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/explorbot-cli.js +2 -1
- 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/bin/prima-cli.js +2 -1
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +4 -3
- 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 -0
- package/dist/src/ai/navigator.js +13 -4
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +57 -5
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +53 -8
- 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/rules.js +13 -0
- 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/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/options/decision-model-option.d.ts +6 -0
- package/dist/src/commands/options/decision-model-option.js +10 -0
- package/dist/src/commands/options/index.d.ts +2 -0
- package/dist/src/commands/options/index.js +2 -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 +7 -0
- package/dist/src/config.js +11 -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/knowledge-tracker.js +1 -1
- 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 +8 -2
- 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/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/docs/api-testing/planning.md +1 -1
- package/docs/basics/providers.md +37 -0
- package/docs/index.json +1 -0
- package/docs/reference/commands.md +1 -0
- package/docs/reference/configuration.md +2 -0
- 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/docs/web-testing/decisions.md +108 -0
- package/docs/workflow/agentic-usage.md +1 -0
- package/package.json +4 -3
- 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 +15 -4
- package/src/ai/pilot.ts +58 -5
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +52 -8
- 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/rules.ts +13 -0
- 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/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/options/decision-model-option.ts +11 -0
- package/src/commands/options/index.ts +2 -0
- 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 +11 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/knowledge-tracker.ts +1 -1
- 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 +6 -1
- 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/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
|
@@ -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.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
A decision model answers one closed question about the current state: a yes/no statement, or a pick from a list of options. It returns the answer together with its probability. Explorbot uses it to settle narrow choices quickly and cheaply that would otherwise need a call to a larger model or a guess.
|
|
4
|
+
|
|
5
|
+
The decision model is optional. Without it Explorbot behaves exactly as described elsewhere in these docs. Setup is covered in [AI providers](../basics/providers.md#decision-model).
|
|
6
|
+
|
|
7
|
+
## Decision rules
|
|
8
|
+
|
|
9
|
+
Every question has one of two shapes:
|
|
10
|
+
|
|
11
|
+
| Shape | Asked as | Answer |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Statement** | "The page shows that this claim is true." | Probability that the statement holds |
|
|
14
|
+
| **Choice** | "Which listed element does the intent name?" plus numbered options | The chosen option and its probability |
|
|
15
|
+
|
|
16
|
+
A choice always includes an extra *undecided* option, so the model can say that none of the options fits.
|
|
17
|
+
|
|
18
|
+
Explorbot acts on an answer only when it is **approved**:
|
|
19
|
+
|
|
20
|
+
- a statement's probability is above **70%**, or
|
|
21
|
+
- a choice picked a real option, not *undecided*, with a probability above **70%**.
|
|
22
|
+
|
|
23
|
+
Anything else counts as **rejected**, including:
|
|
24
|
+
|
|
25
|
+
- a confident "no", or any probability of 70% or lower
|
|
26
|
+
- *undecided*
|
|
27
|
+
- a choice offered with fewer than two options
|
|
28
|
+
- a request timeout (15 seconds) or a failed request
|
|
29
|
+
- `direct: false` in the config, for built-in decision points
|
|
30
|
+
|
|
31
|
+
A rejected answer never means "confidently no". It means Explorbot has no usable answer, so it takes its usual path. Every question is worded so that approving it lets Explorbot take a shortcut and rejecting it keeps the usual path. A wrong or unsure answer can therefore cost a shortcut, but never a check.
|
|
32
|
+
|
|
33
|
+
## Built-in decision points
|
|
34
|
+
|
|
35
|
+
These are fixed places in the code where Explorbot asks the decision model before doing the usual, more expensive work. Setting `direct: false` turns them off.
|
|
36
|
+
|
|
37
|
+
### Tester: locator matches several elements
|
|
38
|
+
|
|
39
|
+
When a click or fill fails because the locator matched more than one element, Explorbot lists the matches and asks which one the step meant.
|
|
40
|
+
|
|
41
|
+
- **Approved:** the Tester is told which element to use and how to target it with `elementIndex`.
|
|
42
|
+
- **Fallback:** the Tester receives the numbered list of matches and picks one itself, narrows the locator, or uses a visual click.
|
|
43
|
+
|
|
44
|
+
### Pilot: progress review
|
|
45
|
+
|
|
46
|
+
Every few steps, Pilot reviews the Tester's recent actions with the `agenticModel`. First it asks whether the run is moving toward its goal and can continue unsupervised for now.
|
|
47
|
+
|
|
48
|
+
- **Approved:** the review is skipped and the Tester continues. This saves one `agenticModel` call.
|
|
49
|
+
- **Fallback:** Pilot runs its full review as usual.
|
|
50
|
+
|
|
51
|
+
### Navigator: claim already verified
|
|
52
|
+
|
|
53
|
+
Before Navigator writes assertions for a claim, it asks whether a claim it has already verified on this page means the same thing, even if it is worded differently.
|
|
54
|
+
|
|
55
|
+
- **Approved:** the claim counts as verified and no assertions are generated or run.
|
|
56
|
+
- **Fallback:** Navigator verifies the claim with assertions as usual.
|
|
57
|
+
|
|
58
|
+
The new claim is not added to the page's verified claims. Only a claim backed by assertions that actually ran is recorded as verified.
|
|
59
|
+
|
|
60
|
+
### Navigator: claim no assertion can express
|
|
61
|
+
|
|
62
|
+
Some claims cannot be turned into an assertion. When that happens, Navigator asks whether the page shows the claim is true.
|
|
63
|
+
|
|
64
|
+
- **Approved:** `verify` reports that the page appears to confirm the claim. The result is marked as a judgement, kept apart from assertions that ran, and the Tester is asked to restate the claim so it can be asserted.
|
|
65
|
+
- **Fallback:** `verify` reports that no assertion could express the claim, and suggests restating it or checking it with `see`.
|
|
66
|
+
|
|
67
|
+
In both cases `verify` returns a failure, because nothing ran in the browser.
|
|
68
|
+
|
|
69
|
+
### Prima: `go` to a described page
|
|
70
|
+
|
|
71
|
+
When `prima go` gets a description of a page instead of a URL, it asks which control on the current page leads there.
|
|
72
|
+
|
|
73
|
+
- **Approved:** Prima clicks that control. It then asks a second question: whether the new page shows the target. Only when that is approved too does Prima report success.
|
|
74
|
+
- **Fallback:** if the choice is rejected, or the click fails, or the page doesn't change, or arrival is not confirmed, Prima hands off to the Navigator as usual.
|
|
75
|
+
|
|
76
|
+
### Prima: `check` expected outcomes
|
|
77
|
+
|
|
78
|
+
When `prima check` settles the expected outcomes of a finished run without a screenshot, it asks what the run established about each outcome: that it happened, or that it did not.
|
|
79
|
+
|
|
80
|
+
- **Approved:** that outcome is settled as passed or failed without calling the `agenticModel`.
|
|
81
|
+
- **Fallback:** the remaining outcomes are settled by the `agenticModel` as usual. When a screenshot is available, all outcomes go to the vision model and the decision model is not asked.
|
|
82
|
+
|
|
83
|
+
## The `judge` tool
|
|
84
|
+
|
|
85
|
+
With `tool: true`, Tester and Pilot get a `judge` tool. They use it when a step depends on reading the page rather than running a command. The tool takes:
|
|
86
|
+
|
|
87
|
+
| Argument | Description |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `question` | A statement to confirm, or the question a list of options answers |
|
|
90
|
+
| `options` | Possible answers. Leave it out to confirm a statement |
|
|
91
|
+
| `context` | Anything the page itself doesn't show |
|
|
92
|
+
|
|
93
|
+
The model supplies only the question. The tool gathers the rest of the state itself: the current scenario, the compact ARIA snapshot of the page (up to 12,000 characters), and the last eight test steps.
|
|
94
|
+
|
|
95
|
+
- **Approved:** the tool returns the answer and its confidence.
|
|
96
|
+
- **Rejected:** the tool returns "Not confirmed" and tells the agent to gather more context or try another route. "Not confirmed" means the page doesn't settle the question. It does not mean the statement is false.
|
|
97
|
+
|
|
98
|
+
`tool` and `direct` are independent. For example, `tool: false` keeps the built-in decision points but stops the agents from asking questions of their own.
|
|
99
|
+
|
|
100
|
+
## Where it is not used
|
|
101
|
+
|
|
102
|
+
- **Pilot's verdict on a test.** Whether a scenario passed, failed or was impossible is always decided by the `agenticModel`.
|
|
103
|
+
- **Filtering experience.** One question per stored experience block, on every prompt, would cost more than it saves.
|
|
104
|
+
- **Generation, screenshots and tool calling.** The decision model reads text and answers questions. It does not write, look at images or act on the page.
|
|
105
|
+
|
|
106
|
+
## Tracing
|
|
107
|
+
|
|
108
|
+
Each question is recorded as a `judge.decide` span with the question, the answer and its confidence. A failed request also records the reason. The TUI shows `⚖️ Asking judge...` while a question is in flight. See [Observability](../contributing/observability.md) for setting up tracing.
|
|
@@ -52,6 +52,7 @@ No `init`, no config file, no project directory, no model IDs to look up. These
|
|
|
52
52
|
| `EXPLORBOT_URL` | yes | Base URL to test; the API boat reads it as the base endpoint |
|
|
53
53
|
| `EXPLORBOT_VISION_MODEL` | no | Screenshot analysis; overrides the provider recommendation |
|
|
54
54
|
| `EXPLORBOT_AGENTIC_MODEL` | no | Captain and Pilot decisions; overrides the provider recommendation |
|
|
55
|
+
| `EXPLORBOT_DECISION_MODEL` | no | Turns on the decision model, as provider/model-id (openrouter/typesafe/jev-1.13 or typesafe/jev-latest); overrides ai.decisionModel |
|
|
55
56
|
| `EXPLORBOT_OUTPUT` | no | Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites |
|
|
56
57
|
| `EXPLORBOT_EPHEMERAL` | no | Keep no state between runs — output goes to a fresh temp directory instead of the site dir |
|
|
57
58
|
| `EXPLORBOT_KNOWLEDGE` | no | Inline knowledge text, applied to every page |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
"bash-tool": "^1.3.15",
|
|
102
103
|
"chalk": "^5.6.2",
|
|
103
104
|
"cli-highlight": "^2.1.11",
|
|
104
|
-
"codeceptjs": "^4.2.0
|
|
105
|
+
"codeceptjs": "^4.2.0",
|
|
105
106
|
"commander": "^14.0.1",
|
|
106
107
|
"debug": "^4.4.3",
|
|
107
108
|
"dedent": "^1.6.0",
|
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
|
@@ -21,6 +21,7 @@ import { normalizeInlineText } from '../utils/strings.ts';
|
|
|
21
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 {
|
|
@@ -570,7 +573,7 @@ class Navigator implements Agent {
|
|
|
570
573
|
const countVisit = (value?: string | null) => {
|
|
571
574
|
if (!value) return;
|
|
572
575
|
const normalized = normalizeUrl(value);
|
|
573
|
-
|
|
576
|
+
visitCounts.set(normalized, (visitCounts.get(normalized) || 0) + 1);
|
|
574
577
|
};
|
|
575
578
|
|
|
576
579
|
for (const transition of history) {
|
|
@@ -581,7 +584,7 @@ class Navigator implements Agent {
|
|
|
581
584
|
if (opts?.visitedUrls) {
|
|
582
585
|
for (const url of opts.visitedUrls) {
|
|
583
586
|
const normalized = normalizeUrl(url);
|
|
584
|
-
if (
|
|
587
|
+
if (!visitCounts.has(normalized)) {
|
|
585
588
|
visitCounts.set(normalized, 1);
|
|
586
589
|
}
|
|
587
590
|
}
|
|
@@ -682,7 +685,7 @@ class Navigator implements Agent {
|
|
|
682
685
|
return suggestion;
|
|
683
686
|
}
|
|
684
687
|
|
|
685
|
-
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 }> {
|
|
686
689
|
tag('info').log('AI Navigator verifying state at', actionResult.url);
|
|
687
690
|
debugLog('Verification message:', message);
|
|
688
691
|
|
|
@@ -692,6 +695,13 @@ class Navigator implements Agent {
|
|
|
692
695
|
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
693
696
|
}
|
|
694
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
|
+
|
|
695
705
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
696
706
|
let experience = '';
|
|
697
707
|
|
|
@@ -832,7 +842,8 @@ class Navigator implements Agent {
|
|
|
832
842
|
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
833
843
|
if (inexpressible) {
|
|
834
844
|
tag('warning').log('No assertion could express this claim');
|
|
835
|
-
|
|
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 };
|
|
836
847
|
}
|
|
837
848
|
|
|
838
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 {
|
|
@@ -548,6 +555,9 @@ export class Pilot implements Agent {
|
|
|
548
555
|
const actionsContext = this.formatActions(toolCalls);
|
|
549
556
|
const stateContext = this.buildStateContext(currentState);
|
|
550
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
|
+
|
|
551
561
|
const hasFailures = toolCalls.length === 0 || toolCalls.some((t) => !t.wasSuccessful);
|
|
552
562
|
|
|
553
563
|
const text = await this.sendToPilot(
|
|
@@ -592,7 +602,11 @@ export class Pilot implements Agent {
|
|
|
592
602
|
|
|
593
603
|
let undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
|
|
594
604
|
if (image) undecided = task.expected;
|
|
595
|
-
|
|
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) }));
|
|
596
610
|
|
|
597
611
|
const schema = z.object({
|
|
598
612
|
outcomes: z.array(
|
|
@@ -665,6 +679,8 @@ export class Pilot implements Agent {
|
|
|
665
679
|
|
|
666
680
|
const judged = new Map((response?.object?.outcomes || []).map((outcome: any) => [outcome.expectation, outcome]));
|
|
667
681
|
return task.expected.map((text) => {
|
|
682
|
+
const byJudge = settledByJudge.get(text);
|
|
683
|
+
if (byJudge) return { text, status: byJudge };
|
|
668
684
|
if (!undecided.includes(text)) return { text, status: decided(text) };
|
|
669
685
|
const outcome = judged.get(text) as { status: SettledStatus; evidence?: string } | undefined;
|
|
670
686
|
if (!outcome) return { text, status: 'unverified' as SettledStatus };
|
|
@@ -672,6 +688,22 @@ export class Pilot implements Agent {
|
|
|
672
688
|
});
|
|
673
689
|
}
|
|
674
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
|
+
|
|
675
707
|
private formatExpectations(task: Test): string {
|
|
676
708
|
const checked = task.getCheckedExpectations();
|
|
677
709
|
const remaining = task.getRemainingExpectations();
|
|
@@ -690,6 +722,7 @@ export class Pilot implements Agent {
|
|
|
690
722
|
this.conversation!.addUserText(finalUserText);
|
|
691
723
|
|
|
692
724
|
const tools = { ...this.pickPlanningTools(), ...this.buildFishermanTools(opts.task) };
|
|
725
|
+
const preparedCount = opts.task.preparedData.length;
|
|
693
726
|
|
|
694
727
|
const result = await this.provider.invokeConversation(this.conversation!, tools, {
|
|
695
728
|
maxToolRoundtrips: opts.maxToolRoundtrips ?? 0,
|
|
@@ -698,7 +731,7 @@ export class Pilot implements Agent {
|
|
|
698
731
|
stopWhen: () => opts.task.hasFinished,
|
|
699
732
|
telemetry: { functionId },
|
|
700
733
|
});
|
|
701
|
-
const text = result?.response?.text || '';
|
|
734
|
+
const text = this.announcePreparedData(result?.response?.text || '', opts.task, preparedCount);
|
|
702
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 }));
|
|
703
736
|
if (learned.length === 0) return text;
|
|
704
737
|
opts.task.applyExperience(learned);
|
|
@@ -714,6 +747,24 @@ export class Pilot implements Agent {
|
|
|
714
747
|
`;
|
|
715
748
|
}
|
|
716
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
|
+
|
|
717
768
|
private getExperienceToc(): string {
|
|
718
769
|
const state = this.stateManager.getCurrentState();
|
|
719
770
|
if (!state) return '';
|
|
@@ -721,7 +772,7 @@ export class Pilot implements Agent {
|
|
|
721
772
|
}
|
|
722
773
|
|
|
723
774
|
private pickPlanningTools() {
|
|
724
|
-
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser } = this.agentTools ?? {};
|
|
775
|
+
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser, judge } = this.agentTools ?? {};
|
|
725
776
|
const planning: Record<string, unknown> = {};
|
|
726
777
|
if (see) planning.see = see;
|
|
727
778
|
if (context) planning.context = context;
|
|
@@ -731,6 +782,7 @@ export class Pilot implements Agent {
|
|
|
731
782
|
if (xpathCheck) planning.xpathCheck = xpathCheck;
|
|
732
783
|
if (learnExperience) planning.learnExperience = learnExperience;
|
|
733
784
|
if (askUser) planning.askUser = askUser;
|
|
785
|
+
if (judge) planning.judge = judge;
|
|
734
786
|
withdrawVisionTools(planning);
|
|
735
787
|
return planning;
|
|
736
788
|
}
|
|
@@ -777,6 +829,7 @@ export class Pilot implements Agent {
|
|
|
777
829
|
});
|
|
778
830
|
const stepText = `Precondition: created ${items.join(', ')}`;
|
|
779
831
|
task.addStep(stepText);
|
|
832
|
+
task.preparedData.push(...items);
|
|
780
833
|
tag('success').log(stepText);
|
|
781
834
|
|
|
782
835
|
return { noted: true, prepared: true, created: result.created };
|
|
@@ -1157,7 +1210,7 @@ export class Pilot implements Agent {
|
|
|
1157
1210
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1158
1211
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1159
1212
|
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1160
|
-
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.
|
|
1161
1214
|
|
|
1162
1215
|
${capabilityGroundingRule}
|
|
1163
1216
|
|