explorbot 0.2.4 → 0.2.5
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/bin/explorbot-cli.ts +19 -7
- package/boat/api-tester/src/cli.ts +17 -0
- package/boat/doc-collector/src/cli.ts +14 -1
- package/boat/prima/src/cli.ts +24 -12
- package/boat/prima/src/envelope.ts +35 -13
- package/boat/prima/src/prima.ts +61 -41
- package/dist/bin/explorbot-cli.js +19 -7
- package/dist/boat/api-tester/src/cli.js +17 -0
- package/dist/boat/doc-collector/src/cli.js +14 -1
- package/dist/boat/prima/src/cli.js +19 -7
- package/dist/boat/prima/src/envelope.js +32 -8
- package/dist/boat/prima/src/prima.js +57 -39
- package/dist/package.json +1 -1
- package/dist/src/action.js +5 -1
- package/dist/src/ai/navigator.d.ts +27 -0
- package/dist/src/ai/navigator.js +227 -175
- package/dist/src/ai/pilot.d.ts +7 -4
- package/dist/src/ai/pilot.js +50 -8
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +12 -21
- package/dist/src/ai/researcher/cache.d.ts +2 -0
- package/dist/src/ai/researcher/cache.js +10 -2
- package/dist/src/ai/researcher.js +2 -1
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/tester.d.ts +5 -2
- package/dist/src/ai/tester.js +17 -13
- package/dist/src/ai/tools.js +4 -1
- package/dist/src/commands/config-command.d.ts +51 -0
- package/dist/src/commands/config-command.js +117 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/config.d.ts +8 -1
- package/dist/src/config.js +40 -0
- package/dist/src/explorbot.js +4 -1
- package/dist/src/remote.d.ts +3 -2
- package/dist/src/remote.js +8 -2
- package/dist/src/state-manager.d.ts +1 -1
- package/dist/src/state-manager.js +3 -1
- package/dist/src/test-plan.d.ts +1 -0
- package/dist/src/test-plan.js +19 -0
- package/dist/src/utils/logger.d.ts +1 -1
- package/dist/src/utils/logger.js +8 -0
- package/docs/index.json +2 -1
- package/docs/reference/commands.md +3 -0
- package/docs/reference/websocket.md +50 -0
- package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
- package/package.json +1 -1
- package/src/action.ts +5 -1
- package/src/ai/navigator.ts +241 -178
- package/src/ai/pilot.ts +63 -12
- package/src/ai/provider.ts +12 -20
- package/src/ai/researcher/cache.ts +12 -2
- package/src/ai/researcher.ts +2 -1
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/tester.ts +20 -12
- package/src/ai/tools.ts +4 -1
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/config.ts +45 -1
- package/src/explorbot.ts +3 -1
- package/src/remote.ts +8 -2
- package/src/state-manager.ts +5 -2
- package/src/test-plan.ts +20 -0
- package/src/utils/logger.ts +9 -1
|
@@ -20,16 +20,26 @@ const helpContract = dedent `
|
|
|
20
20
|
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
21
21
|
`;
|
|
22
22
|
const checkHelp = dedent `
|
|
23
|
-
check takes an outcome rather than a click path, and works out how to reach it.
|
|
23
|
+
check takes an outcome rather than a click path, and works out how to reach it. It runs
|
|
24
|
+
on the page you are already on and never reloads it, so an open dialog survives the check.
|
|
24
25
|
--expected one outcome the run must reach, repeatable for several. Without it the
|
|
25
26
|
scenario text is the single expected outcome. Each comes back under
|
|
26
|
-
### Expected outcomes as PASSED, FAILED or not verified
|
|
27
|
-
means the run never checked it, which is not the same
|
|
27
|
+
### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
|
|
28
|
+
"not verified" means the run never checked it, which is not the same
|
|
29
|
+
as false.
|
|
30
|
+
Outcomes are settled against a screenshot of the whole page: what a user can see is
|
|
31
|
+
the proof, and the run log only says what was done. CONTRADICTION means the two
|
|
32
|
+
disagree - reported with both sides rather than settled one way, and ### Artifacts
|
|
33
|
+
then names the html, aria and screenshot on disk so you can judge it yourself. Not
|
|
34
|
+
finding something in the picture is not enough on its own; that is "not verified".
|
|
35
|
+
ok: follows those outcomes - false when one FAILED or CONTRADICTED, or when the run
|
|
36
|
+
could not complete, which is reported as such rather than as an app failure.
|
|
28
37
|
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
29
38
|
`;
|
|
30
39
|
const doHelp = dedent `
|
|
31
|
-
Each instruction is numbered and accounted for: ### Steps reports each as ok or
|
|
32
|
-
|
|
40
|
+
Each instruction is numbered and accounted for: ### Steps reports each as ok, FAIL or ??.
|
|
41
|
+
?? means the action ran but the run ended without confirming that instruction - read the
|
|
42
|
+
steps above it. Only FAIL and an instruction the page could not carry out fail the command.
|
|
33
43
|
Nothing runs past the last instruction given. A whole remaining sequence in one call is
|
|
34
44
|
what makes this tier cheap.
|
|
35
45
|
`;
|
|
@@ -165,10 +175,12 @@ export function createPrimaCommands(name = 'prima') {
|
|
|
165
175
|
options.baseUrl = target;
|
|
166
176
|
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
167
177
|
});
|
|
168
|
-
addCommonOptions(cmd.command('config').description('Show
|
|
178
|
+
addCommonOptions(cmd.command('config').description('Show models, config file and paths used by this run'))
|
|
179
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
180
|
+
.action(async (options) => {
|
|
169
181
|
setQuietMode(!isVerboseMode());
|
|
170
182
|
const prima = primaFor(options);
|
|
171
|
-
console.log(await prima.config().catch((error) => browserErrorMessage(error)));
|
|
183
|
+
console.log(await prima.config(options.json).catch((error) => browserErrorMessage(error)));
|
|
172
184
|
await prima.stop().catch(() => { });
|
|
173
185
|
process.exit(0);
|
|
174
186
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const EXPECTATION_LABELS = {
|
|
4
|
-
passed: 'PASSED
|
|
5
|
-
failed: 'FAILED
|
|
6
|
-
unverified: 'not verified',
|
|
4
|
+
passed: 'PASSED ',
|
|
5
|
+
failed: 'FAILED ',
|
|
6
|
+
unverified: 'not verified ',
|
|
7
|
+
contradiction: 'CONTRADICTION',
|
|
7
8
|
};
|
|
8
9
|
export function renderEnvelope(data) {
|
|
9
|
-
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
10
|
+
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderWarning(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
10
11
|
return sections.filter((section) => section).join('\n\n');
|
|
11
12
|
}
|
|
12
13
|
export function writeArtifacts(dir, snapshot) {
|
|
@@ -17,6 +18,10 @@ export function writeArtifacts(dir, snapshot) {
|
|
|
17
18
|
};
|
|
18
19
|
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
19
20
|
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
21
|
+
if (snapshot.screenshot) {
|
|
22
|
+
paths.screenshot = path.resolve(dir, 'page.png');
|
|
23
|
+
writeFileSync(paths.screenshot, snapshot.screenshot);
|
|
24
|
+
}
|
|
20
25
|
if (!snapshot.requests.length)
|
|
21
26
|
return paths;
|
|
22
27
|
paths.network = path.resolve(dir, 'network.jsonl');
|
|
@@ -55,7 +60,12 @@ function renderSteps(data) {
|
|
|
55
60
|
return null;
|
|
56
61
|
const lines = [];
|
|
57
62
|
data.steps.forEach((step, index) => {
|
|
58
|
-
|
|
63
|
+
let mark = 'FAIL';
|
|
64
|
+
if (step.ok)
|
|
65
|
+
mark = 'ok ';
|
|
66
|
+
if (step.unconfirmed)
|
|
67
|
+
mark = '?? ';
|
|
68
|
+
lines.push(`${index + 1}. ${mark} ${step.label}`);
|
|
59
69
|
for (const line of (step.proof || '').split('\n').filter(Boolean))
|
|
60
70
|
lines.push(` ${line}`);
|
|
61
71
|
});
|
|
@@ -66,9 +76,21 @@ function renderSteps(data) {
|
|
|
66
76
|
function renderExpectations(data) {
|
|
67
77
|
if (!data.expectations?.length)
|
|
68
78
|
return null;
|
|
69
|
-
const lines =
|
|
79
|
+
const lines = [];
|
|
80
|
+
data.expectations.forEach((expectation, index) => {
|
|
81
|
+
lines.push(`${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
|
|
82
|
+
if (expectation.status !== 'contradiction' && expectation.status !== 'failed')
|
|
83
|
+
return;
|
|
84
|
+
for (const line of (expectation.evidence || '').split('\n').filter(Boolean))
|
|
85
|
+
lines.push(` ${line}`);
|
|
86
|
+
});
|
|
70
87
|
return section('Expected outcomes', lines.join('\n'));
|
|
71
88
|
}
|
|
89
|
+
function renderWarning(data) {
|
|
90
|
+
if (!data.warning)
|
|
91
|
+
return null;
|
|
92
|
+
return section('Warning', data.warning);
|
|
93
|
+
}
|
|
72
94
|
function renderOutcome(data) {
|
|
73
95
|
if (data.answer)
|
|
74
96
|
return section('Answer', data.answer);
|
|
@@ -127,9 +149,11 @@ function tabsLabel(tabs) {
|
|
|
127
149
|
export function renderArtifacts(data) {
|
|
128
150
|
if (!data.artifacts)
|
|
129
151
|
return null;
|
|
130
|
-
const lines = [`aria:
|
|
152
|
+
const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
|
|
153
|
+
if (data.artifacts.screenshot)
|
|
154
|
+
lines.push(`screenshot: ${data.artifacts.screenshot}`);
|
|
131
155
|
if (data.artifacts.network)
|
|
132
|
-
lines.push(`network:
|
|
156
|
+
lines.push(`network: ${data.artifacts.network}`);
|
|
133
157
|
return section('Artifacts', lines.join('\n'));
|
|
134
158
|
}
|
|
135
159
|
function align(label, marker, width) {
|
|
@@ -4,28 +4,29 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { tool } from 'ai';
|
|
6
6
|
import dedent from 'dedent';
|
|
7
|
-
import { z } from 'zod';
|
|
8
7
|
import * as playwright from 'playwright';
|
|
8
|
+
import { z } from 'zod';
|
|
9
9
|
import { ActionResult } from "../../../src/action-result.js";
|
|
10
|
+
import { getPreviousResearch } from "../../../src/ai/researcher/cache.js";
|
|
10
11
|
import { actionRule, locatorRule } from "../../../src/ai/rules.js";
|
|
11
12
|
import { createAgentTools, createCodeceptJSTools } from "../../../src/ai/tools.js";
|
|
12
13
|
import { getAliveEndpoint, launchServer, listInstances, stopServer } from "../../../src/browser-server.js";
|
|
13
|
-
import {
|
|
14
|
+
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
14
15
|
import { ConfigMissingError, ConfigParser, outputPath } from "../../../src/config.js";
|
|
15
16
|
import { ExplorBot } from "../../../src/explorbot.js";
|
|
17
|
+
import { listSites } from "../../../src/global-config.js";
|
|
16
18
|
import { Reporter } from "../../../src/reporter.js";
|
|
17
19
|
import { Stats } from "../../../src/stats.js";
|
|
18
20
|
import { Task, Test, TestResult } from "../../../src/test-plan.js";
|
|
19
|
-
import { getPreviousResearch } from "../../../src/ai/researcher/cache.js";
|
|
20
21
|
import { compactAriaSnapshot } from "../../../src/utils/aria.js";
|
|
21
|
-
import { mdq } from "../../../src/utils/markdown-query.js";
|
|
22
22
|
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
23
23
|
import { pluralize } from "../../../src/utils/logger.js";
|
|
24
|
+
import { mdq } from "../../../src/utils/markdown-query.js";
|
|
24
25
|
import { safeFilename } from "../../../src/utils/strings.js";
|
|
25
26
|
import { writeArtifacts } from "./envelope.js";
|
|
26
27
|
import { isFunctionExpression, takePwValue, toCodeceptWrapper } from "./pw-parser.js";
|
|
27
|
-
import { latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from "./session-log.js";
|
|
28
28
|
import { readDescriptors, selectDescriptor } from "./pw-registry.js";
|
|
29
|
+
import { latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from "./session-log.js";
|
|
29
30
|
const TESTER_ONLY_TOOLS = ['learnExperience', 'askUser'];
|
|
30
31
|
const ITERATIONS_PER_INSTRUCTION = 2;
|
|
31
32
|
const MAX_INSTRUCTION_ITERATIONS = 24;
|
|
@@ -36,7 +37,7 @@ const AI_AGENT_NAME = 'prima';
|
|
|
36
37
|
const CONNECT_TIMEOUT = 3000;
|
|
37
38
|
const requireLib = createRequire(import.meta.url);
|
|
38
39
|
const VOLATILE_COLUMNS = ['CSS', 'XPath', 'Coordinates', 'eidx'];
|
|
39
|
-
const UNACCOUNTED = { open: '
|
|
40
|
+
const UNACCOUNTED = { open: 'the run ended without confirming this one — the actions above are everything that ran' };
|
|
40
41
|
function dropVolatileColumns(markdown) {
|
|
41
42
|
return mdq(markdown)
|
|
42
43
|
.query('table')
|
|
@@ -67,6 +68,7 @@ export class Prima {
|
|
|
67
68
|
server = null;
|
|
68
69
|
attached = null;
|
|
69
70
|
session = null;
|
|
71
|
+
artifacts;
|
|
70
72
|
constructor(options = {}) {
|
|
71
73
|
this.options = options;
|
|
72
74
|
this.bot = new ExplorBot({
|
|
@@ -208,18 +210,18 @@ export class Prima {
|
|
|
208
210
|
}
|
|
209
211
|
if (aiError)
|
|
210
212
|
return this.failureEnvelope(command, aiError, previousState);
|
|
211
|
-
if (
|
|
213
|
+
if (trace.length && ledger.some((entry) => entry.status === 'open')) {
|
|
212
214
|
await this.settleLedger(conversation, provider, ledger, trace);
|
|
213
215
|
}
|
|
214
216
|
const unfinished = ledger.filter((entry) => entry.status !== 'done');
|
|
215
|
-
const steps = [...trace, ...ledger.filter((entry) => entry.status === 'open').map((entry) => ({ label:
|
|
217
|
+
const steps = [...trace, ...ledger.filter((entry) => entry.status === 'open').map((entry) => ({ label: entry.text, ok: false, unconfirmed: true, proof: UNACCOUNTED.open }))];
|
|
216
218
|
if (failure && unfinished.length) {
|
|
217
219
|
const envelope = await this.failureEnvelope(command, failure.message, previousState);
|
|
218
220
|
envelope.steps = steps;
|
|
219
221
|
envelope.stepFiles = this.statusDir();
|
|
220
222
|
return envelope;
|
|
221
223
|
}
|
|
222
|
-
if (!
|
|
224
|
+
if (!trace.length && unfinished.length === ledger.length) {
|
|
223
225
|
const reason = ['No action was performed for these instructions on the current page.', narration].filter(Boolean).join(' ');
|
|
224
226
|
return this.failureEnvelope(command, reason, previousState);
|
|
225
227
|
}
|
|
@@ -230,10 +232,10 @@ export class Prima {
|
|
|
230
232
|
// the step log already reports every action and what it changed
|
|
231
233
|
envelope.used = undefined;
|
|
232
234
|
envelope.changes = undefined;
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
+
const blocked = ledger.filter((entry) => entry.status === 'blocked');
|
|
236
|
+
if (blocked.length) {
|
|
235
237
|
envelope.ok = false;
|
|
236
|
-
envelope.failure = { error:
|
|
238
|
+
envelope.failure = { error: blocked.map((entry) => `blocked: ${entry.text}${entry.proof ? ` — ${entry.proof}` : ''}`).join('\n') };
|
|
237
239
|
}
|
|
238
240
|
return envelope;
|
|
239
241
|
}
|
|
@@ -281,7 +283,13 @@ export class Prima {
|
|
|
281
283
|
instructions have moved it on, and something you confirmed earlier stays confirmed even if it is gone.
|
|
282
284
|
completed() for those, blocked() for the ones the page could not do. Report every one — nothing else runs after this.
|
|
283
285
|
`);
|
|
284
|
-
|
|
286
|
+
let settleError = null;
|
|
287
|
+
const invoked = await provider.invokeConversation(conversation, { completed: this.completedTool(), blocked: this.blockedTool() }, { maxToolRoundtrips: 2, toolChoice: 'required', agentName: AI_AGENT_NAME }).catch((error) => {
|
|
288
|
+
settleError = error;
|
|
289
|
+
return null;
|
|
290
|
+
});
|
|
291
|
+
if (settleError)
|
|
292
|
+
trace.push({ label: 'settling which instructions were satisfied', ok: false, proof: browserErrorMessage(settleError) });
|
|
285
293
|
for (const execution of invoked?.toolExecutions || []) {
|
|
286
294
|
this.applyLedgerReport(execution, ledger, trace);
|
|
287
295
|
}
|
|
@@ -305,17 +313,32 @@ export class Prima {
|
|
|
305
313
|
const outcomes = expected.length ? expected : [scenario];
|
|
306
314
|
const test = new Test(scenario, 'normal', outcomes, previousState?.url || this.options.url || '');
|
|
307
315
|
const tester = this.bot.agentTester();
|
|
308
|
-
|
|
316
|
+
await tester.test(test, { startOnCurrentPage: true });
|
|
309
317
|
const notes = Object.values(test.notes || {});
|
|
310
|
-
const result = await this.capturedResult(this.bot.stateManager().getCurrentState());
|
|
311
|
-
const envelope = await this.reportEnvelope(command, result, previousState, {
|
|
318
|
+
const result = await this.capturedResult(this.bot.stateManager().getCurrentState(), { screenshot: this.visionEnabled() });
|
|
319
|
+
const envelope = await this.reportEnvelope(command, result, previousState, {});
|
|
312
320
|
const recorded = notes.filter((note) => !note.observation && !outcomes.includes(note.message));
|
|
313
321
|
const failed = recorded.filter((note) => note.status === TestResult.FAILED);
|
|
314
322
|
envelope.steps = failed.map((note) => ({ label: note.message, ok: false, proof: note.log || '' }));
|
|
315
323
|
const routine = recorded.length - failed.length;
|
|
316
324
|
if (routine)
|
|
317
325
|
envelope.steps.push({ label: `${routine} further ${pluralize(routine, 'step')} ran without failing — prima status ${envelope.status} for the full log`, ok: true, proof: '' });
|
|
318
|
-
envelope.expectations = await this.bot.agentPilot().settleExpectations(test);
|
|
326
|
+
envelope.expectations = await this.bot.agentPilot().settleExpectations(test, result);
|
|
327
|
+
if (!result.screenshot || !this.visionEnabled()) {
|
|
328
|
+
envelope.warning = 'These outcomes were settled from the run log alone — no screenshot backed them. Set ai.visionModel, or check anything visual with prima ask.';
|
|
329
|
+
}
|
|
330
|
+
const unreached = envelope.expectations.filter((expectation) => expectation.status === 'failed');
|
|
331
|
+
const contradicted = envelope.expectations.filter((expectation) => expectation.status === 'contradiction');
|
|
332
|
+
envelope.ok = !unreached.length && !contradicted.length;
|
|
333
|
+
const problems = [...unreached.map((expectation) => `not reached: ${expectation.text}`), ...contradicted.map((expectation) => `the picture and the run disagree about: ${expectation.text}`)];
|
|
334
|
+
if (problems.length)
|
|
335
|
+
envelope.failure = { error: problems.join('\n') };
|
|
336
|
+
if (contradicted.length)
|
|
337
|
+
envelope.artifacts = this.artifacts;
|
|
338
|
+
if (!test.hasFinished || test.isSkipped) {
|
|
339
|
+
envelope.ok = false;
|
|
340
|
+
envelope.failure = { error: `the run did not complete, so it established nothing about the app: ${notes.at(-1)?.message || 'no steps were recorded'}` };
|
|
341
|
+
}
|
|
319
342
|
const observations = notes.filter((note) => note.observation).map((note) => note.message);
|
|
320
343
|
if (observations.length)
|
|
321
344
|
envelope.answer = ['Page problems noticed while running, not step failures:', ...observations.map((line) => `- ${line}`)].join('\n');
|
|
@@ -338,7 +361,14 @@ export class Prima {
|
|
|
338
361
|
const previousState = this.bot.stateManager().getCurrentState();
|
|
339
362
|
const result = await this.capturedResult(previousState);
|
|
340
363
|
const verification = await this.bot.agentNavigator().verifyState(assertion, result);
|
|
341
|
-
|
|
364
|
+
const outcome = { assertions: verification.results || [] };
|
|
365
|
+
if (verification.inexpressible) {
|
|
366
|
+
const question = `Judging only from the screenshot, is this true of the page: "${assertion}"? Answer true, false or undetermined, and say what settles it.`;
|
|
367
|
+
const seen = await this.visionAnswer(question, await this.capturedResult(previousState, { screenshot: this.visionEnabled() }));
|
|
368
|
+
if (seen)
|
|
369
|
+
outcome.answer = `No assertion could express this claim, so it was judged from a screenshot instead.\n\n${seen}`;
|
|
370
|
+
}
|
|
371
|
+
return this.reportEnvelope(command, result, previousState, outcome);
|
|
342
372
|
}
|
|
343
373
|
async research(opts = {}) {
|
|
344
374
|
const flags = [opts.data && '--data', opts.deep && '--deep', opts.fresh && '--fresh'].filter(Boolean);
|
|
@@ -398,27 +428,13 @@ export class Prima {
|
|
|
398
428
|
}
|
|
399
429
|
return stopped;
|
|
400
430
|
}
|
|
401
|
-
async config() {
|
|
431
|
+
async config(json) {
|
|
402
432
|
const [site] = listSites();
|
|
403
433
|
if (site && !this.configBaseUrl())
|
|
404
434
|
this.sessionUrl = site.url;
|
|
405
435
|
const config = await this.loadConfig();
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
return model;
|
|
409
|
-
return model?.modelId || model?.model || 'unknown';
|
|
410
|
-
};
|
|
411
|
-
const ai = config.ai || {};
|
|
412
|
-
const roles = [
|
|
413
|
-
['model', ai.model],
|
|
414
|
-
['agenticModel', ai.agenticModel],
|
|
415
|
-
['visionModel', ai.visionModel],
|
|
416
|
-
];
|
|
417
|
-
const lines = roles.filter(([, model]) => model).map(([role, model]) => `${role.padEnd(14)} ${named(model)}`);
|
|
418
|
-
lines.push(`config ${ConfigParser.getInstance().getConfigPath() || 'built-in defaults'}`);
|
|
419
|
-
if (ai.langfuse?.enabled)
|
|
420
|
-
lines.push('telemetry langfuse');
|
|
421
|
-
return lines.join('\n');
|
|
436
|
+
const parser = ConfigParser.getInstance();
|
|
437
|
+
return ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json });
|
|
422
438
|
}
|
|
423
439
|
record(envelope, durationMs) {
|
|
424
440
|
if (!this.session)
|
|
@@ -706,6 +722,7 @@ export class Prima {
|
|
|
706
722
|
<proof>
|
|
707
723
|
An instruction is done only when a change on the page shows it. After each action read the reported change and decide which part of it proves the instruction.
|
|
708
724
|
That part is what completed() takes as its proof. Do not restate the action as if it were the outcome.
|
|
725
|
+
How much of the page moved is not evidence of whether it happened — a change confined to one region proves an instruction as well as one that redraws everything.
|
|
709
726
|
An instruction that only inspects the page is satisfied by what you can see, including seeing that something is absent — those need no action at all.
|
|
710
727
|
</proof>
|
|
711
728
|
|
|
@@ -856,6 +873,8 @@ export class Prima {
|
|
|
856
873
|
visionEnabled() {
|
|
857
874
|
if (this.options.noVision)
|
|
858
875
|
return false;
|
|
876
|
+
if (Stats.visionDisabled)
|
|
877
|
+
return false;
|
|
859
878
|
return this.bot.getProvider().hasVision?.() === true;
|
|
860
879
|
}
|
|
861
880
|
async answer(question, result) {
|
|
@@ -976,7 +995,6 @@ export class Prima {
|
|
|
976
995
|
ok: true,
|
|
977
996
|
command: `status ${hash}`,
|
|
978
997
|
page: saved.page,
|
|
979
|
-
changes: saved.changes,
|
|
980
998
|
instance: await this.instanceInfo(),
|
|
981
999
|
artifacts: { aria: path.join(dir, 'aria.yml'), html: path.join(dir, 'page.html') },
|
|
982
1000
|
};
|
|
@@ -984,7 +1002,7 @@ export class Prima {
|
|
|
984
1002
|
async saveStatus(result) {
|
|
985
1003
|
const hash = this.statusHash();
|
|
986
1004
|
await this.writeSnapshot(result);
|
|
987
|
-
writeFileSync(path.join(this.statusDir(hash), 'status.json'), JSON.stringify({ page: this.pageBlock(result, null)
|
|
1005
|
+
writeFileSync(path.join(this.statusDir(hash), 'status.json'), JSON.stringify({ page: this.pageBlock(result, null) }), 'utf-8');
|
|
988
1006
|
return hash;
|
|
989
1007
|
}
|
|
990
1008
|
async writeStepFiles(index, label, diff) {
|
|
@@ -1001,12 +1019,12 @@ export class Prima {
|
|
|
1001
1019
|
writeFileSync(`${stem}.diff.yaml`, diff, 'utf-8');
|
|
1002
1020
|
}
|
|
1003
1021
|
async writeSnapshot(result) {
|
|
1004
|
-
writeArtifacts(this.statusDir(), {
|
|
1022
|
+
this.artifacts = writeArtifacts(this.statusDir(), {
|
|
1005
1023
|
aria: result.ariaSnapshot,
|
|
1006
1024
|
html: await result.combinedHtml(),
|
|
1025
|
+
screenshot: result.screenshot,
|
|
1007
1026
|
requests: this.bot.requestStore().getRequests(),
|
|
1008
1027
|
});
|
|
1009
|
-
return undefined;
|
|
1010
1028
|
}
|
|
1011
1029
|
statusHash() {
|
|
1012
1030
|
this.hash ||= createHash('sha1')
|
package/dist/package.json
CHANGED
package/dist/src/action.js
CHANGED
|
@@ -50,6 +50,7 @@ class Action {
|
|
|
50
50
|
await this.actor.saveScreenshot(filename);
|
|
51
51
|
if (currentState)
|
|
52
52
|
currentState.screenshotFile = filename;
|
|
53
|
+
tag('data').log('screenshot', { path: outputPath('states', filename) });
|
|
53
54
|
return filename;
|
|
54
55
|
}
|
|
55
56
|
catch (err) {
|
|
@@ -97,7 +98,10 @@ class Action {
|
|
|
97
98
|
const screenshotPath = join(statesDir, filename);
|
|
98
99
|
screenshotFile = await page
|
|
99
100
|
?.screenshot({ path: screenshotPath, fullPage: true })
|
|
100
|
-
.then(() =>
|
|
101
|
+
.then(() => {
|
|
102
|
+
tag('data').log('screenshot', { path: screenshotPath });
|
|
103
|
+
return filename;
|
|
104
|
+
})
|
|
101
105
|
.catch((err) => {
|
|
102
106
|
debugLog('Screenshot failed, continuing without it:', err);
|
|
103
107
|
return undefined;
|
|
@@ -15,6 +15,7 @@ declare class Navigator implements Agent {
|
|
|
15
15
|
experienceTracker: ExperienceTracker;
|
|
16
16
|
hooksRunner: HooksRunner;
|
|
17
17
|
MAX_ATTEMPTS: number;
|
|
18
|
+
lastFailureReason: string | null;
|
|
18
19
|
systemPrompt: string;
|
|
19
20
|
freeSailSystemPrompt: string;
|
|
20
21
|
explorer: Explorer;
|
|
@@ -25,10 +26,16 @@ declare class Navigator implements Agent {
|
|
|
25
26
|
get verifyTimeout(): number;
|
|
26
27
|
getBaseOrigin(): string | null;
|
|
27
28
|
getComparableCurrentUrl(stateManager: any, expectedUrl: string): string;
|
|
29
|
+
comparableUrl(state: {
|
|
30
|
+
url?: string;
|
|
31
|
+
fullUrl?: string;
|
|
32
|
+
}, expectedUrl: string): string;
|
|
28
33
|
isSameExpectedOrigin(expectedUrl: string, stateManager: any): boolean;
|
|
29
34
|
isOnExpectedPage(expectedUrl: string, stateManager: any): boolean;
|
|
30
35
|
visit(url: string): Promise<void>;
|
|
31
36
|
visitOnce(url: string): Promise<void>;
|
|
37
|
+
navigationError(url: string, fallback: string): Error;
|
|
38
|
+
failureReason(stopReason: string | null, knowledge: string, url: string): string | null;
|
|
32
39
|
resolveState(message: string, actionResult: ActionResult, opts?: {
|
|
33
40
|
action?: Action;
|
|
34
41
|
expectedUrl?: string;
|
|
@@ -37,6 +44,20 @@ declare class Navigator implements Agent {
|
|
|
37
44
|
error?: string;
|
|
38
45
|
}) => void;
|
|
39
46
|
}): Promise<boolean>;
|
|
47
|
+
buildResolutionPrompt(message: string, actionResult: ActionResult): Promise<string>;
|
|
48
|
+
buildRetryFeedback(failures: BatchFailure[], includeHtml: boolean, actionResult: ActionResult): Promise<string>;
|
|
49
|
+
executeAttempt(action: Action, codeBlock: string, message: string): Promise<{
|
|
50
|
+
ok: boolean;
|
|
51
|
+
error?: string;
|
|
52
|
+
}>;
|
|
53
|
+
verifyNavigation(action: Action, expectedUrl: string): Promise<{
|
|
54
|
+
freshState: ActionResult;
|
|
55
|
+
urlMatches: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
ariaDiff(freshState: ActionResult, previous: ActionResult): Promise<string | null>;
|
|
58
|
+
saveFlow(message: string, expectedUrl: string | undefined, actionResult: ActionResult, progressBlocks: string[]): void;
|
|
59
|
+
rescueDelayedRedirect(action: Action, expectedUrl: string): Promise<boolean>;
|
|
60
|
+
askUserToResolve(action: Action, message: string, expectedUrl: string | undefined, stopReason: string | null): Promise<boolean>;
|
|
40
61
|
buildExperienceTools(): {
|
|
41
62
|
learnExperience: unknown;
|
|
42
63
|
} | undefined;
|
|
@@ -61,6 +82,12 @@ declare class Navigator implements Agent {
|
|
|
61
82
|
}>;
|
|
62
83
|
checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean;
|
|
63
84
|
}
|
|
85
|
+
type BatchFailure = {
|
|
86
|
+
code: string;
|
|
87
|
+
error: string;
|
|
88
|
+
ariaChanges?: string | null;
|
|
89
|
+
urlAfter?: string;
|
|
90
|
+
};
|
|
64
91
|
export type AssertionResult = {
|
|
65
92
|
code: string;
|
|
66
93
|
passed: boolean;
|