explorbot 0.5.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +495 -482
  3. package/boat/api-tester/src/ai/curler.ts +1 -1
  4. package/boat/api-tester/src/cli.ts +4 -4
  5. package/boat/api-tester/src/commands/init-command.ts +2 -0
  6. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -1
  7. package/boat/doc-collector/src/screenshots.ts +1 -1
  8. package/boat/prima/bin/prima-cli.ts +2 -1
  9. package/dist/bin/explorbot-cli.js +456 -444
  10. package/dist/boat/api-tester/bin/apibot-cli.js +2 -1
  11. package/dist/boat/api-tester/src/ai/curler.js +1 -1
  12. package/dist/boat/api-tester/src/cli.js +4 -4
  13. package/dist/boat/api-tester/src/commands/init-command.js +2 -0
  14. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -1
  15. package/dist/boat/prima/bin/prima-cli.js +2 -1
  16. package/dist/models.json +1 -1
  17. package/dist/package.json +2 -1
  18. package/dist/rules/navigator/output.md +2 -8
  19. package/dist/rules/navigator/verification-actions.md +6 -2
  20. package/dist/rules/planner/ignored-pages.md +1 -0
  21. package/dist/src/action-result.js +1 -1
  22. package/dist/src/action.d.ts +0 -1
  23. package/dist/src/action.js +0 -4
  24. package/dist/src/ai/captain/idle-mode.js +42 -26
  25. package/dist/src/ai/captain/mixin.d.ts +2 -0
  26. package/dist/src/ai/captain/web-mode.js +11 -0
  27. package/dist/src/ai/captain.d.ts +6 -4
  28. package/dist/src/ai/captain.js +25 -1
  29. package/dist/src/ai/driller.js +1 -1
  30. package/dist/src/ai/experience-compactor.js +2 -2
  31. package/dist/src/ai/judge.d.ts +7 -0
  32. package/dist/src/ai/judge.js +8 -1
  33. package/dist/src/ai/navigator.d.ts +1 -0
  34. package/dist/src/ai/navigator.js +11 -7
  35. package/dist/src/ai/planner/subpages.js +1 -1
  36. package/dist/src/ai/planner.d.ts +12 -5
  37. package/dist/src/ai/planner.js +138 -86
  38. package/dist/src/ai/provider.d.ts +1 -1
  39. package/dist/src/ai/provider.js +16 -5
  40. package/dist/src/ai/rerunner.js +1 -1
  41. package/dist/src/ai/researcher/cache.js +14 -6
  42. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
  43. package/dist/src/ai/researcher/deep-analysis.js +7 -2
  44. package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
  45. package/dist/src/ai/researcher.js +44 -24
  46. package/dist/src/ai/rules.js +6 -7
  47. package/dist/src/ai/scout.d.ts +12 -2
  48. package/dist/src/ai/scout.js +103 -8
  49. package/dist/src/ai/tester.d.ts +9 -5
  50. package/dist/src/ai/tester.js +22 -42
  51. package/dist/src/ai/tools.js +10 -8
  52. package/dist/src/application-spec.d.ts +2 -0
  53. package/dist/src/application-spec.js +17 -12
  54. package/dist/src/commands/context-experience-command.js +0 -1
  55. package/dist/src/commands/context-knowledge-command.js +0 -1
  56. package/dist/src/commands/explore-command.d.ts +3 -2
  57. package/dist/src/commands/explore-command.js +35 -23
  58. package/dist/src/commands/help-json-command.js +3 -1
  59. package/dist/src/commands/init-command.js +8 -4
  60. package/dist/src/commands/learn-command.js +0 -2
  61. package/dist/src/commands/options/auth-option.d.ts +6 -0
  62. package/dist/src/commands/options/auth-option.js +14 -0
  63. package/dist/src/commands/options/index.d.ts +2 -0
  64. package/dist/src/commands/options/index.js +2 -0
  65. package/dist/src/commands/plan-command.d.ts +3 -0
  66. package/dist/src/commands/plan-command.js +43 -17
  67. package/dist/src/commands/plan-save-command.js +6 -17
  68. package/dist/src/commands/plans-command.js +1 -1
  69. package/dist/src/commands/test-command.js +12 -2
  70. package/dist/src/components/App.d.ts +1 -2
  71. package/dist/src/components/App.js +1 -3
  72. package/dist/src/components/InputPane.js +0 -5
  73. package/dist/src/config.d.ts +3 -2
  74. package/dist/src/config.js +3 -12
  75. package/dist/src/explorbot.d.ts +3 -2
  76. package/dist/src/explorbot.js +32 -10
  77. package/dist/src/explorer.js +6 -7
  78. package/dist/src/knowledge-tracker.d.ts +2 -2
  79. package/dist/src/knowledge-tracker.js +18 -6
  80. package/dist/src/playwright-recorder.d.ts +0 -1
  81. package/dist/src/playwright-recorder.js +0 -2
  82. package/dist/src/reporter.d.ts +1 -1
  83. package/dist/src/reporter.js +3 -3
  84. package/dist/src/test-plan.js +1 -1
  85. package/dist/src/utils/error-page.d.ts +17 -0
  86. package/dist/src/utils/error-page.js +53 -2
  87. package/dist/src/utils/html-diff.d.ts +1 -2
  88. package/dist/src/utils/html-diff.js +4 -44
  89. package/dist/src/utils/logger.js +27 -10
  90. package/dist/src/utils/markdown-terminal.js +1 -2
  91. package/dist/src/utils/next-steps.js +20 -16
  92. package/dist/src/utils/page-readiness.d.ts +2 -1
  93. package/dist/src/utils/page-readiness.js +24 -5
  94. package/dist/src/utils/test-plan-markdown.d.ts +3 -0
  95. package/dist/src/utils/test-plan-markdown.js +35 -4
  96. package/dist/src/utils/url-matcher.d.ts +1 -0
  97. package/dist/src/utils/url-matcher.js +5 -0
  98. package/docs/basics/providers.md +1 -1
  99. package/docs/contributing/regression-tests.md +19 -4
  100. package/docs/reference/commands.md +18 -2
  101. package/docs/reference/configuration.md +1 -1
  102. package/docs/web-testing/agents.md +8 -0
  103. package/docs/web-testing/customization.md +2 -0
  104. package/docs/web-testing/researcher.md +1 -1
  105. package/docs/workflow/agentic-usage.md +7 -0
  106. package/docs/workflow/knowledge.md +6 -0
  107. package/models.json +1 -1
  108. package/package.json +2 -1
  109. package/rules/navigator/output.md +2 -8
  110. package/rules/navigator/verification-actions.md +6 -2
  111. package/rules/planner/ignored-pages.md +1 -0
  112. package/src/action-result.ts +1 -2
  113. package/src/action.ts +0 -5
  114. package/src/ai/captain/idle-mode.ts +38 -26
  115. package/src/ai/captain/mixin.ts +2 -1
  116. package/src/ai/captain/web-mode.ts +12 -0
  117. package/src/ai/captain.ts +25 -7
  118. package/src/ai/driller.ts +1 -2
  119. package/src/ai/experience-compactor.ts +2 -2
  120. package/src/ai/judge.ts +10 -1
  121. package/src/ai/navigator.ts +12 -7
  122. package/src/ai/planner/subpages.ts +1 -2
  123. package/src/ai/planner.ts +140 -92
  124. package/src/ai/provider.ts +15 -6
  125. package/src/ai/rerunner.ts +1 -2
  126. package/src/ai/researcher/cache.ts +16 -7
  127. package/src/ai/researcher/deep-analysis.ts +7 -2
  128. package/src/ai/researcher/fingerprint-worker.ts +5 -5
  129. package/src/ai/researcher.ts +44 -28
  130. package/src/ai/rules.ts +6 -7
  131. package/src/ai/scout.ts +98 -10
  132. package/src/ai/tester.ts +29 -47
  133. package/src/ai/tools.ts +11 -9
  134. package/src/application-spec.ts +19 -13
  135. package/src/commands/clean-command.ts +1 -1
  136. package/src/commands/context-command.ts +0 -1
  137. package/src/commands/context-experience-command.ts +0 -1
  138. package/src/commands/context-knowledge-command.ts +0 -1
  139. package/src/commands/explore-command.ts +36 -23
  140. package/src/commands/help-json-command.ts +8 -6
  141. package/src/commands/init-command.ts +10 -3
  142. package/src/commands/learn-command.ts +0 -2
  143. package/src/commands/options/auth-option.ts +15 -0
  144. package/src/commands/options/index.ts +2 -0
  145. package/src/commands/plan-command.ts +44 -17
  146. package/src/commands/plan-save-command.ts +6 -17
  147. package/src/commands/plans-command.ts +1 -1
  148. package/src/commands/test-command.ts +11 -2
  149. package/src/components/App.tsx +2 -6
  150. package/src/components/InputPane.tsx +0 -5
  151. package/src/config.ts +5 -16
  152. package/src/experience-tracker.ts +1 -1
  153. package/src/explorbot.ts +31 -11
  154. package/src/explorer.ts +6 -7
  155. package/src/knowledge-tracker.ts +18 -7
  156. package/src/playwright-recorder.ts +0 -2
  157. package/src/reporter.ts +3 -4
  158. package/src/test-plan.ts +1 -2
  159. package/src/utils/error-page.ts +64 -2
  160. package/src/utils/html-diff.ts +4 -56
  161. package/src/utils/html.ts +0 -1
  162. package/src/utils/logger.ts +30 -12
  163. package/src/utils/markdown-terminal.ts +1 -2
  164. package/src/utils/next-steps.ts +19 -17
  165. package/src/utils/page-readiness.ts +33 -7
  166. package/src/utils/test-plan-markdown.ts +36 -4
  167. package/src/utils/url-matcher.ts +6 -0
package/src/action.ts CHANGED
@@ -34,8 +34,6 @@ class Action {
34
34
  public actionResult: ActionResult | null = null;
35
35
  private config: ExplorbotConfig;
36
36
 
37
- // action info
38
- private action: string | null = null;
39
37
  public lastError: Error | null = null;
40
38
  public playwrightHelper: any;
41
39
  public playwrightGroupId: string | null = null;
@@ -389,8 +387,6 @@ class Action {
389
387
  }
390
388
 
391
389
  private async executeOnce(code: string, verbatim = false): Promise<Action> {
392
- let error: Error | null = null;
393
-
394
390
  setActivity('🔎 Browsing...', 'action');
395
391
 
396
392
  let codeString = code.replace(/^\(I\) => /, '').trim();
@@ -441,7 +437,6 @@ class Action {
441
437
  this.assertionSteps = assertionSteps;
442
438
  } catch (err) {
443
439
  debugLog('Action error', errorToString(err));
444
- error = err as Error;
445
440
  if (!isPlaywright) {
446
441
  await recorder.reset();
447
442
  await recorder.start();
@@ -3,7 +3,9 @@ import { createBashTool } from 'bash-tool';
3
3
  import dedent from 'dedent';
4
4
  import { z } from 'zod';
5
5
  import { ConfigParser } from '../../config.ts';
6
- import { Test } from '../../test-plan.ts';
6
+ import { Plan, Test } from '../../test-plan.ts';
7
+ import { getStyles } from '../planner/styles.ts';
8
+ import { PlanningBlockedError } from '../../utils/error-page.ts';
7
9
  import { listRecentArtifacts, readCaptainFile } from './file-tools.ts';
8
10
  import { type Constructor, type ModeContext, resolveProjectRoot } from './mixin.ts';
9
11
 
@@ -36,11 +38,18 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
36
38
  }
37
39
 
38
40
  const tools: Record<string, any> = {
39
- updatePlan: tool({
40
- description: 'Update the current plan by replacing or appending tests',
41
+ plan: tool({
42
+ description: dedent`
43
+ Plan tests for the current page. Adds them to the current plan when it is for this page, otherwise starts a new plan.
44
+ Without tests, the Planner writes the tests. With tests, exactly those tests are added and the Planner does not run: use it for tests the user dictates.
45
+ Returns how many tests were added, or why planning failed.
46
+ `,
41
47
  inputSchema: z.object({
42
- action: z.enum(['replace', 'append']).optional().describe('replace clears existing tests, append keeps them'),
43
- title: z.string().optional().describe('New plan title'),
48
+ feature: z.string().optional().describe('Focus area for the Planner'),
49
+ style: z
50
+ .enum(Object.keys(getStyles()) as [string, ...string[]])
51
+ .optional()
52
+ .describe('Planning style for the Planner; omit to let it choose'),
44
53
  tests: z
45
54
  .array(
46
55
  z.object({
@@ -49,33 +58,36 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
49
58
  expected: z.array(z.string()).optional(),
50
59
  })
51
60
  )
52
- .optional(),
61
+ .optional()
62
+ .describe('Tests to add as written, without running the Planner'),
63
+ replace: z.boolean().optional().describe('Remove the tests already in this page plan first'),
64
+ title: z.string().optional().describe('New plan title'),
53
65
  }),
54
- execute: async ({ action, title, tests }) => {
66
+ execute: async ({ feature, style, tests, replace, title }) => {
67
+ if (ctx.blocker instanceof PlanningBlockedError) {
68
+ feature = ctx.blocker.feature;
69
+ style = ctx.blocker.style;
70
+ }
71
+ const url = ctx.explorBot.stateManager().getCurrentState()?.url || '';
55
72
  let plan = ctx.explorBot.getCurrentPlan();
56
- if (!plan) {
57
- plan = await ctx.explorBot.plan();
73
+ if (plan?.url && plan.url !== url) plan = undefined;
74
+ if (replace) plan?.tests.splice(0);
75
+ const before = new Set(plan?.tests ?? []);
76
+ if (!tests?.length) {
77
+ plan = await ctx.explorBot.plan(feature, { style });
78
+ const error = ctx.explorBot.lastPlanError;
79
+ if (error) return { success: false, message: error.message };
58
80
  }
59
81
  if (!plan) {
60
- return { success: false, message: 'Plan unavailable' };
61
- }
62
- if (title) {
63
- plan.title = title;
82
+ plan = new Plan(url);
83
+ ctx.explorBot.setCurrentPlan(plan);
64
84
  }
65
- if (tests?.length) {
66
- if (!action || action === 'replace') {
67
- plan.tests.length = 0;
68
- }
69
- const currentUrl = ctx.explorBot.stateManager().getCurrentState()?.url || '';
70
- for (const testInput of tests) {
71
- const priority = testInput.priority || 'normal';
72
- const expected = testInput.expected?.length ? testInput.expected : [];
73
- const test = new Test(testInput.scenario, priority, expected, currentUrl);
74
- plan.addTest(test);
75
- }
85
+ for (const t of tests ?? []) {
86
+ plan.addTest(new Test(t.scenario, t.priority || 'normal', t.expected ?? [], url));
76
87
  }
88
+ if (title) plan.title = title;
77
89
  plan.updateStatus();
78
- return { success: true, tests: plan.tests.length };
90
+ return { success: true, testsAdded: plan.tests.filter((t) => !before.has(t)).length, tests: plan.tests.length };
79
91
  },
80
92
  }),
81
93
  project: tool({
@@ -144,7 +156,7 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
144
156
 
145
157
  return dedent`
146
158
  <idle_capabilities>
147
- - Plan management: updatePlan() — replace or append tests in the current plan
159
+ - Plan management: plan() — run the Planner on the current page, or add tests you write yourself
148
160
  - readFile() — read specific report, plan, log, generated test, knowledge, or experience file content
149
161
  - bash() — discover files and inspect file metadata
150
162
  - READ from: ${knowledgeDir}/, ${experienceDir}/, ${outputDir}/
@@ -1,7 +1,7 @@
1
- import { dirname } from 'node:path';
2
1
  import { ConfigParser } from '../../config.ts';
3
2
  import type { ExplorBot } from '../../explorbot.ts';
4
3
  import type { Task } from '../../test-plan.ts';
4
+ import type { BlockedError } from '../../utils/error-page.ts';
5
5
  import { createDebug } from '../../utils/logger.js';
6
6
 
7
7
  export type Constructor<T = object> = new (...args: any[]) => T;
@@ -13,6 +13,7 @@ export type CaptainMode = 'idle' | 'web' | 'test' | 'heal';
13
13
  export interface ModeContext {
14
14
  explorBot: ExplorBot;
15
15
  task: Task;
16
+ blocker: BlockedError | null;
16
17
  }
17
18
 
18
19
  export function resolveProjectRoot(): string | null {
@@ -1,6 +1,7 @@
1
1
  import { tool } from 'ai';
2
2
  import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
+ import { pageEvidence } from '../judge.ts';
4
5
  import { actionRule, locatorRule, sectionContextRule } from '../rules.ts';
5
6
  import { createAgentTools, createCodeceptJSTools } from '../tools.ts';
6
7
  import { type Constructor, type ModeContext, debugLog } from './mixin.ts';
@@ -37,6 +38,16 @@ export function WithWebMode<T extends Constructor>(Base: T) {
37
38
  },
38
39
  }),
39
40
 
41
+ oracle: tool({
42
+ description: 'Ask a yes/no question about the current page. Much cheaper than see or context.',
43
+ inputSchema: z.object({ question: z.string() }),
44
+ execute: async ({ question }) => {
45
+ const captured = await ctx.explorBot.getExplorer().capture({ screenshot: true });
46
+ const holds = await ctx.explorBot.getProvider().oracle(question, pageEvidence(captured), { agentName: 'captain', screenshot: captured.screenshot, telemetry: { functionId: 'captain.oracle' } });
47
+ return { success: holds !== null, question, answer: holds };
48
+ },
49
+ }),
50
+
40
51
  browser: tool({
41
52
  description: dedent`
42
53
  Direct browser access via Playwright. Use for diagnostics and browser management.
@@ -157,6 +168,7 @@ export function WithWebMode<T extends Constructor>(Base: T) {
157
168
  - click() accepts array of commands to try in order — include ARIA, CSS, XPath variants
158
169
  - If click() fails with all provided commands, use visualClick() tool as fallback
159
170
  - When an action fails or result is unexpected, investigate or inform the user instead of retrying silently
171
+ - For a quick yes/no question about the page, use oracle instead of see or context
160
172
  `;
161
173
  }
162
174
  };
package/src/ai/captain.ts CHANGED
@@ -7,6 +7,7 @@ import type { ExplorBot } from '../explorbot.ts';
7
7
  import type { WebPageState } from '../state-manager.ts';
8
8
  import { Task, Test } from '../test-plan.ts';
9
9
  import { formatHeadings } from '../utils/context-formatter.ts';
10
+ import { BlockedError } from '../utils/error-page.ts';
10
11
  import { HooksRunner } from '../utils/hooks-runner.ts';
11
12
  import { startLogCapture, stopLogCapture, tag } from '../utils/logger.js';
12
13
  import { loop } from '../utils/loop.js';
@@ -21,6 +22,7 @@ import type { Navigator } from './navigator.ts';
21
22
  import type { Provider } from './provider.ts';
22
23
  import { Researcher } from './researcher.ts';
23
24
  import { TaskAgent } from './task-agent.ts';
25
+ import type { SupervisorAction } from './tester.ts';
24
26
  import { createLearnExperienceTool, withdrawVisionTools } from './tools.ts';
25
27
 
26
28
  const MAX_STEPS = 15;
@@ -35,6 +37,9 @@ export class Captain extends CaptainBase implements Agent {
35
37
  private hooksRunner: HooksRunner | null = null;
36
38
  private commandExecutor: ((cmd: string) => Promise<void>) | null = null;
37
39
  private commandDescriptions: { name: string; description: string; options: string }[] = [];
40
+ private unresolvedBlockers = new Set<string>();
41
+ private handling = false;
42
+ private blocker: BlockedError | null = null;
38
43
 
39
44
  constructor(explorBot: ExplorBot) {
40
45
  super();
@@ -123,6 +128,7 @@ export class Captain extends CaptainBase implements Agent {
123
128
  - After a successful action, if the pageDiff confirms the goal, call done() immediately — do not verify with see() or context() unless the user explicitly asked for verification
124
129
  - Prefer completing in fewer tool calls over thoroughness
125
130
  - NEVER run tests unless the user explicitly asks
131
+ - When an agent reports it is blocked, decide which agent can resolve it and delegate to that agent
126
132
  - If you are answering with information rather than completing a browser action, include the actual user-facing answer in done({ details }). Do not only say that it was shown or explained.
127
133
  ${mode === 'web' || mode === 'heal' ? this.webModeRules() : ''}
128
134
  ${mode === 'test' || mode === 'heal' ? this.testModeRules() : ''}
@@ -304,7 +310,7 @@ export class Captain extends CaptainBase implements Agent {
304
310
 
305
311
  private async tools(task: Task, onDone: (summary: string) => void) {
306
312
  const mode = this.getMode();
307
- const ctx: ModeContext = { explorBot: this.explorBot, task };
313
+ const ctx: ModeContext = { explorBot: this.explorBot, task, blocker: this.blocker };
308
314
  const core = this.coreTools(task, onDone);
309
315
  const idle = await this.idleModeTools(ctx);
310
316
 
@@ -437,6 +443,7 @@ export class Captain extends CaptainBase implements Agent {
437
443
 
438
444
  conversation.addUserText(initialPrompt);
439
445
 
446
+ this.handling = true;
440
447
  await loop(
441
448
  async ({ stop, iteration, userInput }) => {
442
449
  debugLog(`Captain iteration ${iteration}`);
@@ -510,7 +517,9 @@ export class Captain extends CaptainBase implements Agent {
510
517
  stop();
511
518
  },
512
519
  }
513
- );
520
+ ).finally(() => {
521
+ this.handling = false;
522
+ });
514
523
 
515
524
  const finalUrl = stateManager.getCurrentState()?.url || startUrl;
516
525
  await this.getHooksRunner().runAfterHook('captain', finalUrl);
@@ -523,15 +532,24 @@ export class Captain extends CaptainBase implements Agent {
523
532
 
524
533
  return null;
525
534
  }
535
+
536
+ async resolveBlocker(error: unknown, resolved: () => boolean | Promise<boolean>): Promise<boolean> {
537
+ if (!(error instanceof BlockedError) || this.handling) return false;
538
+ const key = `${error.name} ${error.url} ${error.blocker}`;
539
+ if (this.unresolvedBlockers.has(key)) return false;
540
+ tag('info').log(`Captain takes over: ${error.message}`);
541
+ this.blocker = error;
542
+ await this.handle(error.request, { reset: true }).finally(() => {
543
+ this.blocker = null;
544
+ });
545
+ if (await resolved()) return true;
546
+ this.unresolvedBlockers.add(key);
547
+ return false;
548
+ }
526
549
  }
527
550
 
528
551
  export default Captain;
529
552
 
530
- interface SupervisorAction {
531
- action: 'inject' | 'stop' | 'pass' | 'skip';
532
- message: string;
533
- }
534
-
535
553
  interface ExecutionRecoveryAction {
536
554
  action: 'continue' | 'stop';
537
555
  message: string;
package/src/ai/driller.ts CHANGED
@@ -15,7 +15,6 @@ import { eidxInContainer } from '../utils/web-eidx.ts';
15
15
  import { WebElement } from '../utils/web-element.ts';
16
16
  import type { Agent, AgentDeps } from './agent.ts';
17
17
  import type { Navigator } from './navigator.ts';
18
- import type { Provider } from './provider.ts';
19
18
  import { drillLocatorRule } from './rules.ts';
20
19
  import { TaskAgent, isInteractive } from './task-agent.ts';
21
20
  import { createCodeceptJSTools, createLearnExperienceTool } from './tools.ts';
@@ -177,7 +176,7 @@ export class Driller extends TaskAgent implements Agent {
177
176
  setActivity(`${this.emoji} Capturing annotated page state...`, 'action');
178
177
  const action = this.explorer.action();
179
178
  try {
180
- const annotated = await Promise.race([
179
+ await Promise.race([
181
180
  this.explorer.withPage(annotatePageElements),
182
181
  new Promise<never>((_, reject) => {
183
182
  setTimeout(() => reject(new Error('annotateElements timeout')), 15000);
@@ -1,5 +1,5 @@
1
1
  import { unlinkSync } from 'node:fs';
2
- import { basename } from 'node:path';
2
+ import { basename, relative } from 'node:path';
3
3
  import dedent from 'dedent';
4
4
  import { z } from 'zod';
5
5
  import { type ExperienceFile, type ExperienceTracker, RECENT_WINDOW_DAYS } from '../experience-tracker.js';
@@ -322,7 +322,7 @@ export class ExperienceCompactor implements Agent {
322
322
  };
323
323
 
324
324
  this.experienceTracker.writeExperienceFile(targetStateHash, combinedContent, newFrontmatter);
325
- log(`Merged ${group.files.length} experience files into ${targetFile.filePath}`);
325
+ log(`Merged ${group.files.length} experience files into ${relative(process.cwd(), targetFile.filePath)}`);
326
326
  debugLog('New URL pattern:', group.pattern);
327
327
 
328
328
  for (const sourceFile of sourceFiles) {
package/src/ai/judge.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { clearActivity, setActivity } from '../activity.ts';
2
+ import type { ActionResult } from '../action-result.ts';
2
3
  import type { AIConfig } from '../config.ts';
3
4
  import { Observability } from '../observability.ts';
4
- import { createDebug } from '../utils/logger.ts';
5
+ import { createDebug, tag } from '../utils/logger.ts';
5
6
  import { JudgeProvider } from './judge-provider.ts';
6
7
 
7
8
  const debugLog = createDebug('explorbot:judge');
@@ -29,6 +30,8 @@ export class Decision {
29
30
  }
30
31
 
31
32
  export class Judge {
33
+ private failureReported = false;
34
+
32
35
  constructor(
33
36
  private provider: JudgeProvider,
34
37
  private enabled: { tool: boolean; direct: boolean },
@@ -93,6 +96,8 @@ export class Judge {
93
96
 
94
97
  private recordFailure(error: unknown): null {
95
98
  debugLog('judge declined: %s', error);
99
+ if (!this.failureReported) tag('warning').log(`Decision model ${this.provider.model} failed (${error}); continuing without it`);
100
+ this.failureReported = true;
96
101
  const span = Observability.getSpan();
97
102
  span?.setAttribute('ai.telemetry.metadata.judgeError', String(error));
98
103
  span?.setAttribute('langfuse.observation.level', 'ERROR');
@@ -100,3 +105,7 @@ export class Judge {
100
105
  return null;
101
106
  }
102
107
  }
108
+
109
+ export function pageEvidence(actionResult: ActionResult) {
110
+ return { url: actionResult.url, title: actionResult.title, page: actionResult.getCompactARIA().slice(0, JUDGE_PAGE_CAP) };
111
+ }
@@ -13,7 +13,7 @@ import { type StateManager, normalizeUrl } from '../state-manager.js';
13
13
  import { isFatalBrowserError } from '../utils/browser-errors.ts';
14
14
  import { getCliName } from '../utils/cli-name.ts';
15
15
  import { extractCodeBlocks } from '../utils/code-extractor.js';
16
- import { isLoginPage } from '../utils/error-page.ts';
16
+ import { NavigationBlockedError, isLoginPage } from '../utils/error-page.ts';
17
17
  import { HooksRunner } from '../utils/hooks-runner.ts';
18
18
  import { createDebug, pluralize, tag } from '../utils/logger.js';
19
19
  import { loop, pause } from '../utils/loop.js';
@@ -22,7 +22,6 @@ import { normalizeInlineText } from '../utils/strings.ts';
22
22
  import { extractStatePath, isSameHostFamily, matchesNavigationUrl } from '../utils/url-matcher.js';
23
23
  import type { UrlFilter } from '../utils/url-filter.ts';
24
24
  import type { Agent, AgentDeps } from './agent.js';
25
- import type { Conversation } from './conversation.js';
26
25
  import { type Decision, JUDGE_PAGE_CAP, type Judge, UNDECIDED } from './judge.ts';
27
26
  import type { Provider } from './provider.js';
28
27
  import { Researcher } from './researcher.ts';
@@ -31,6 +30,7 @@ import { isInteractive } from './task-agent.js';
31
30
  import { createLearnExperienceTool } from './tools.ts';
32
31
 
33
32
  const debugLog = createDebug('explorbot:navigator');
33
+ const MAX_TRIED_STEPS = 5;
34
34
 
35
35
  class Navigator implements Agent {
36
36
  emoji = '🧭';
@@ -138,6 +138,10 @@ class Navigator implements Agent {
138
138
  return matchesNavigationUrl(expectedUrl, currentUrl);
139
139
  }
140
140
 
141
+ isOn(url: string): boolean {
142
+ return this.isOnExpectedPage(url, this.stateManager);
143
+ }
144
+
141
145
  async visit(url: string, opts: { recover?: boolean } = {}): Promise<boolean> {
142
146
  return this.visitOnce(url, opts.recover !== false);
143
147
  }
@@ -195,8 +199,9 @@ class Navigator implements Agent {
195
199
  }
196
200
 
197
201
  private navigationError(url: string, fallback: string): Error {
198
- if (this.lastFailureReason) return new Error(`Navigation to ${url} failed: ${this.lastFailureReason}`);
199
- return new Error(`Navigation to ${url} failed: ${fallback}`);
202
+ const currentUrl = this.stateManager.getCurrentState()?.url || '';
203
+ const tried = this.executedSteps.slice(-MAX_TRIED_STEPS).map((step) => step.command);
204
+ return new NavigationBlockedError(url, currentUrl, this.lastFailureReason || fallback, tried);
200
205
  }
201
206
 
202
207
  private failureReason(stopReason: string | null, knowledge: string, url: string): string | null {
@@ -402,7 +407,7 @@ class Navigator implements Agent {
402
407
  Propose different solutions to achieve the result.
403
408
  Solution should be valid CodeceptJS code.
404
409
  Use only data from the <page> context to plan the solution.
405
- The page is given as an ARIA snapshot only: use ARIA and text locators from it.
410
+ The page is given as an ARIA snapshot only: use ARIA and text locators from it; propose CSS/XPath only once <page_html> is provided.
406
411
  Full HTML is provided if these solutions fail.
407
412
  Try various ways to achieve the result
408
413
  </task>
@@ -772,7 +777,7 @@ class Navigator implements Agent {
772
777
  Do not generate assertions that would pass even if the specific claim is false.
773
778
  </task>
774
779
 
775
- ${RulesLoader.loadRules('navigator', ['verification-actions'], actionResult.url || '')}
780
+ ${RulesLoader.loadRules('navigator', ['verification-actions', 'verification-output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.verifyAttempts))}
776
781
 
777
782
  ${experience}
778
783
 
@@ -858,7 +863,7 @@ class Navigator implements Agent {
858
863
 
859
864
  const totalAttempted = Math.min(codeBlocks.length, this.verifyAttempts);
860
865
  let verified = successfulCodes.length > 0 && !failures;
861
- if (successfulCodes.length && failures) verified = await this.provider.oracle(message, { assertions: results.map(({ code, passed, error }) => ({ code, passed, error })) }, { agentName: 'navigator', telemetry: { functionId: 'navigator.verifyState' } });
866
+ if (successfulCodes.length && failures) verified = (await this.provider.oracle(message, { assertions: results.map(({ code, passed, error }) => ({ code, passed, error })) }, { agentName: 'navigator', telemetry: { functionId: 'navigator.verifyState' } })) === true;
862
867
  if (alreadyVerified) verified = true;
863
868
 
864
869
  const inexpressible = !alreadyVerified && totalAttempted === 0;
@@ -3,7 +3,6 @@ import { z } from 'zod';
3
3
  import { normalizeUrl } from '../../state-manager.ts';
4
4
  import type { StateManager } from '../../state-manager.ts';
5
5
  import type { Plan } from '../../test-plan.ts';
6
- import { tag } from '../../utils/logger.ts';
7
6
  import { isSamePageFamily } from '../../utils/url-matcher.ts';
8
7
  import type { Provider } from '../provider.ts';
9
8
  import type { Constructor } from '../researcher/mixin.ts';
@@ -85,7 +84,7 @@ export function WithSubPages<T extends Constructor>(Base: T) {
85
84
  async pickNextSubPage(candidates: SubPageCandidate[]): Promise<{ url: string; reason: string } | null> {
86
85
  if (candidates.length === 0) return null;
87
86
 
88
- const plannedEntries = [...planRegistry.entries()].map(([key, record]) => `- ${record.url} (${record.plan.tests.length} tests${record.feature ? `, feature: ${record.feature}` : ''})`).join('\n');
87
+ const plannedEntries = [...planRegistry.entries()].map(([, record]) => `- ${record.url} (${record.plan.tests.length} tests${record.feature ? `, feature: ${record.feature}` : ''})`).join('\n');
89
88
 
90
89
  const candidateList = candidates.map((c) => `- ${c.url} (visits: ${c.visitCount}${c.title ? `, title: "${c.title}"` : ''}${c.h1 ? `, h1: "${c.h1}"` : ''})`).join('\n');
91
90