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.
Files changed (63) hide show
  1. package/bin/explorbot-cli.ts +19 -7
  2. package/boat/api-tester/src/cli.ts +17 -0
  3. package/boat/doc-collector/src/cli.ts +14 -1
  4. package/boat/prima/src/cli.ts +24 -12
  5. package/boat/prima/src/envelope.ts +35 -13
  6. package/boat/prima/src/prima.ts +61 -41
  7. package/dist/bin/explorbot-cli.js +19 -7
  8. package/dist/boat/api-tester/src/cli.js +17 -0
  9. package/dist/boat/doc-collector/src/cli.js +14 -1
  10. package/dist/boat/prima/src/cli.js +19 -7
  11. package/dist/boat/prima/src/envelope.js +32 -8
  12. package/dist/boat/prima/src/prima.js +57 -39
  13. package/dist/package.json +1 -1
  14. package/dist/src/action.js +5 -1
  15. package/dist/src/ai/navigator.d.ts +27 -0
  16. package/dist/src/ai/navigator.js +227 -175
  17. package/dist/src/ai/pilot.d.ts +7 -4
  18. package/dist/src/ai/pilot.js +50 -8
  19. package/dist/src/ai/provider.d.ts +2 -2
  20. package/dist/src/ai/provider.js +12 -21
  21. package/dist/src/ai/researcher/cache.d.ts +2 -0
  22. package/dist/src/ai/researcher/cache.js +10 -2
  23. package/dist/src/ai/researcher.js +2 -1
  24. package/dist/src/ai/session-analyst.js +2 -0
  25. package/dist/src/ai/tester.d.ts +5 -2
  26. package/dist/src/ai/tester.js +17 -13
  27. package/dist/src/ai/tools.js +4 -1
  28. package/dist/src/commands/config-command.d.ts +51 -0
  29. package/dist/src/commands/config-command.js +117 -0
  30. package/dist/src/commands/index.js +2 -0
  31. package/dist/src/config.d.ts +8 -1
  32. package/dist/src/config.js +40 -0
  33. package/dist/src/explorbot.js +4 -1
  34. package/dist/src/remote.d.ts +3 -2
  35. package/dist/src/remote.js +8 -2
  36. package/dist/src/state-manager.d.ts +1 -1
  37. package/dist/src/state-manager.js +3 -1
  38. package/dist/src/test-plan.d.ts +1 -0
  39. package/dist/src/test-plan.js +19 -0
  40. package/dist/src/utils/logger.d.ts +1 -1
  41. package/dist/src/utils/logger.js +8 -0
  42. package/docs/index.json +2 -1
  43. package/docs/reference/commands.md +3 -0
  44. package/docs/reference/websocket.md +50 -0
  45. package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
  46. package/package.json +1 -1
  47. package/src/action.ts +5 -1
  48. package/src/ai/navigator.ts +241 -178
  49. package/src/ai/pilot.ts +63 -12
  50. package/src/ai/provider.ts +12 -20
  51. package/src/ai/researcher/cache.ts +12 -2
  52. package/src/ai/researcher.ts +2 -1
  53. package/src/ai/session-analyst.ts +2 -0
  54. package/src/ai/tester.ts +20 -12
  55. package/src/ai/tools.ts +4 -1
  56. package/src/commands/config-command.ts +146 -0
  57. package/src/commands/index.ts +2 -0
  58. package/src/config.ts +45 -1
  59. package/src/explorbot.ts +3 -1
  60. package/src/remote.ts +8 -2
  61. package/src/state-manager.ts +5 -2
  62. package/src/test-plan.ts +20 -0
  63. 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 - "not verified"
27
- means the run never checked it, which is not the same as false.
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 FAIL
32
- with what proved it. One that could not be carried out fails the command and says why.
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 the AI models prima runs on and the config file they come from')).action(async (options) => {
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
- lines.push(`${index + 1}. ${step.ok ? 'ok ' : 'FAIL'} ${step.label}`);
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 = data.expectations.map((expectation, index) => `${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
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: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
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: ${data.artifacts.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 { listSites } from "../../../src/global-config.js";
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: 'never reported — the run ended with this instruction still 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 (used.length && ledger.some((entry) => entry.status === 'open')) {
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: `unreported: ${entry.text}`, ok: false, proof: UNACCOUNTED.open }))];
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 (!used.length && unfinished.length === ledger.length) {
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 unmet = unfinished.map((entry) => `${entry.status}: ${entry.text}${entry.proof ? ` — ${entry.proof}` : ''}`);
234
- if (unmet.length) {
235
+ const blocked = ledger.filter((entry) => entry.status === 'blocked');
236
+ if (blocked.length) {
235
237
  envelope.ok = false;
236
- envelope.failure = { error: unmet.join('\n') };
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
- const invoked = await provider.invokeConversation(conversation, { completed: this.completedTool(), blocked: this.blockedTool() }, { maxToolRoundtrips: 2, toolChoice: 'required', agentName: AI_AGENT_NAME }).catch(() => null);
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
- const outcome = await tester.test(test);
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, { ok: outcome.success });
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
- return this.reportEnvelope(command, result, previousState, { assertions: verification.results || [] });
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 named = (model) => {
407
- if (typeof model === 'string')
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), changes: compactAriaSnapshot(result.ariaSnapshot, true) }), 'utf-8');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -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(() => filename)
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;