explorbot 0.3.4 → 0.3.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/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -1005,6 +1005,8 @@ export class Pilot {
1005
1005
  state), instruct Tester to verify() and finish(). If goal was already true at the start, propose
1006
1006
  different input data so the test is meaningful. If Tester repeats the same successful action, STOP.
1007
1007
 
1008
+ If needed you should pick the exact item the scenario should act on (from the page, or precondition() one) and pass it to tester
1009
+
1008
1010
  Action classification: GOAL-ADVANCING actions mutate the scenario's subject data (create/edit/delete/submit/verify).
1009
1011
  VIEW-ONLY actions toggle filters/tabs/sort/collapse without changing data. One VIEW-ONLY to reveal a
1010
1012
  target is fine; ≥2 consecutive VIEW-ONLY actions with no GOAL-ADVANCING action in between is thrashing
@@ -24,7 +24,7 @@ const TasksSchema = z.object({
24
24
  planName: z.string().describe('Short descriptive name for the test plan (e.g., "User Authentication Testing", "Product Catalog Navigation", "Form Validation Tests")'),
25
25
  scenarios: z
26
26
  .array(z.object({
27
- scenario: z.string().describe('A single sentence describing what to test'),
27
+ scenario: z.string().describe('A single sentence describing the behavior to test.'),
28
28
  priority: z.enum(['critical', 'important', 'high', 'normal', 'low']).describe('Priority of the task based on business importance'),
29
29
  startUrl: z.string().nullable().describe('Start URL for the test if different from plan URL. Use only stable feature/list/detail pages, not transient create/edit/modal URLs unless the scenario specifically starts inside that form.'),
30
30
  steps: z.array(z.string()).describe('List of steps to perform for this scenario. Each step should be a specific action (e.g., "Open the form", "Enter required data", "Submit the form"). Keep steps atomic and actionable.'),
@@ -2,7 +2,8 @@ import { OpenTelemetry } from '@ai-sdk/otel';
2
2
  import { LangfuseSpanProcessor } from '@langfuse/otel';
3
3
  import { NodeSDK } from '@opentelemetry/sdk-node';
4
4
  import { AsyncLocalStorage } from 'node:async_hooks';
5
- import { generateObject, generateText, isStepCount, registerTelemetry, tool } from 'ai';
5
+ import dedent from 'dedent';
6
+ import { APICallError, generateObject, generateText, isStepCount, registerTelemetry, tool } from 'ai';
6
7
  import { z } from 'zod';
7
8
  import { clearActivity, setActivity } from "../activity.js";
8
9
  import { configuredModels, modelName as getModelName } from '../config.js';
@@ -382,9 +383,18 @@ export class Provider {
382
383
  if (extraStop)
383
384
  stopConditions.push(extraStop);
384
385
  const config = this.buildGenerateConfig({ tools: toolsWithCommentary, maxOutputTokens: 16384, toolChoice: 'auto', experimental_repairToolCall: repairToolCall }, { stopWhen: stopConditions, model }, options);
386
+ let attemptMessages = messages;
387
+ let invalidRequestFeedbackAdded = false;
385
388
  try {
386
389
  const response = await this.withModelRequestSlot(() => withRetry(async () => {
387
- const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000));
390
+ const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages: attemptMessages, ...config, abortSignal: signal }), config.timeout || 30000).catch((error) => {
391
+ if (!invalidRequestFeedbackAdded) {
392
+ const amended = withInvalidRequestFeedback(attemptMessages, error);
393
+ invalidRequestFeedbackAdded = amended !== attemptMessages;
394
+ attemptMessages = amended;
395
+ }
396
+ throw error;
397
+ }));
388
398
  this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
389
399
  const hasToolCall = (result.toolCalls?.length || 0) > 0;
390
400
  if (!result.text && !hasToolCall && result.finishReason === 'length') {
@@ -620,6 +630,22 @@ function repairToolCall(options) {
620
630
  return repairChannelMarker(options);
621
631
  return repairHarmonyChannel(options);
622
632
  }
633
+ function withInvalidRequestFeedback(messages, error) {
634
+ if (!(error instanceof APICallError) || error.statusCode !== 400)
635
+ return messages;
636
+ tag('warning').log('Provider rejected the request as invalid — relaying its reason before the retry');
637
+ return [
638
+ ...messages,
639
+ {
640
+ role: 'user',
641
+ content: dedent `
642
+ The previous request was rejected by the provider as invalid:
643
+ "${error.message}"
644
+ Fix what it describes and re-issue the request.
645
+ `,
646
+ },
647
+ ];
648
+ }
623
649
  function repairChannelMarker({ toolCall, tools }) {
624
650
  const markerIndex = toolCall.toolName.indexOf('<|channel|>');
625
651
  if (markerIndex <= 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
package/src/ai/pilot.ts CHANGED
@@ -1114,6 +1114,8 @@ export class Pilot implements Agent {
1114
1114
  state), instruct Tester to verify() and finish(). If goal was already true at the start, propose
1115
1115
  different input data so the test is meaningful. If Tester repeats the same successful action, STOP.
1116
1116
 
1117
+ If needed you should pick the exact item the scenario should act on (from the page, or precondition() one) and pass it to tester
1118
+
1117
1119
  Action classification: GOAL-ADVANCING actions mutate the scenario's subject data (create/edit/delete/submit/verify).
1118
1120
  VIEW-ONLY actions toggle filters/tabs/sort/collapse without changing data. One VIEW-ONLY to reveal a
1119
1121
  target is fine; ≥2 consecutive VIEW-ONLY actions with no GOAL-ADVANCING action in between is thrashing
package/src/ai/planner.ts CHANGED
@@ -33,7 +33,7 @@ const TasksSchema = z.object({
33
33
  scenarios: z
34
34
  .array(
35
35
  z.object({
36
- scenario: z.string().describe('A single sentence describing what to test'),
36
+ scenario: z.string().describe('A single sentence describing the behavior to test.'),
37
37
  priority: z.enum(['critical', 'important', 'high', 'normal', 'low']).describe('Priority of the task based on business importance'),
38
38
  startUrl: z.string().nullable().describe('Start URL for the test if different from plan URL. Use only stable feature/list/detail pages, not transient create/edit/modal URLs unless the scenario specifically starts inside that form.'),
39
39
  steps: z.array(z.string()).describe('List of steps to perform for this scenario. Each step should be a specific action (e.g., "Open the form", "Enter required data", "Submit the form"). Keep steps atomic and actionable.'),
@@ -2,7 +2,8 @@ import { OpenTelemetry } from '@ai-sdk/otel';
2
2
  import { LangfuseSpanProcessor } from '@langfuse/otel';
3
3
  import { NodeSDK } from '@opentelemetry/sdk-node';
4
4
  import { AsyncLocalStorage } from 'node:async_hooks';
5
- import { generateObject, generateText, isStepCount, registerTelemetry, tool } from 'ai';
5
+ import dedent from 'dedent';
6
+ import { APICallError, generateObject, generateText, isStepCount, registerTelemetry, tool } from 'ai';
6
7
  import type { ModelMessage } from 'ai';
7
8
  import { z } from 'zod';
8
9
  import { clearActivity, setActivity } from '../activity.ts';
@@ -423,10 +424,19 @@ export class Provider {
423
424
  const stopConditions: any[] = [isStepCount(maxRoundtrips)];
424
425
  if (extraStop) stopConditions.push(extraStop);
425
426
  const config = this.buildGenerateConfig({ tools: toolsWithCommentary, maxOutputTokens: 16384, toolChoice: 'auto', experimental_repairToolCall: repairToolCall }, { stopWhen: stopConditions, model }, options);
427
+ let attemptMessages = messages;
428
+ let invalidRequestFeedbackAdded = false;
426
429
  try {
427
430
  const response = await this.withModelRequestSlot(() =>
428
431
  withRetry(async () => {
429
- const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
432
+ const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages: attemptMessages, ...config, abortSignal: signal }), config.timeout || 30000).catch((error) => {
433
+ if (!invalidRequestFeedbackAdded) {
434
+ const amended = withInvalidRequestFeedback(attemptMessages, error);
435
+ invalidRequestFeedbackAdded = amended !== attemptMessages;
436
+ attemptMessages = amended;
437
+ }
438
+ throw error;
439
+ })) as any;
430
440
  this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
431
441
  const hasToolCall = (result.toolCalls?.length || 0) > 0;
432
442
  if (!result.text && !hasToolCall && result.finishReason === 'length') {
@@ -693,6 +703,22 @@ function repairToolCall(options: ToolCallRepairOptions): any | null {
693
703
  return repairHarmonyChannel(options);
694
704
  }
695
705
 
706
+ function withInvalidRequestFeedback(messages: ModelMessage[], error: unknown): ModelMessage[] {
707
+ if (!(error instanceof APICallError) || error.statusCode !== 400) return messages;
708
+ tag('warning').log('Provider rejected the request as invalid — relaying its reason before the retry');
709
+ return [
710
+ ...messages,
711
+ {
712
+ role: 'user',
713
+ content: dedent`
714
+ The previous request was rejected by the provider as invalid:
715
+ "${error.message}"
716
+ Fix what it describes and re-issue the request.
717
+ `,
718
+ },
719
+ ];
720
+ }
721
+
696
722
  function repairChannelMarker({ toolCall, tools }: ToolCallRepairOptions): any | null {
697
723
  const markerIndex = toolCall.toolName.indexOf('<|channel|>');
698
724
  if (markerIndex <= 0) return null;