explorbot 0.4.10 → 0.5.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.
- package/README.md +4 -1
- package/bin/mdq.ts +18 -0
- package/boat/api-tester/src/ai/chief.ts +72 -0
- package/boat/api-tester/src/api-client.ts +37 -0
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/mdq.js +19 -0
- package/dist/boat/api-tester/src/ai/chief.js +69 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +3 -2
- package/dist/src/ai/agent.d.ts +3 -1
- package/dist/src/ai/judge-provider.d.ts +17 -0
- package/dist/src/ai/judge-provider.js +56 -0
- package/dist/src/ai/judge-tool.d.ts +2 -0
- package/dist/src/ai/judge-tool.js +33 -0
- package/dist/src/ai/judge.d.ts +28 -0
- package/dist/src/ai/judge.js +71 -0
- package/dist/src/ai/navigator.d.ts +3 -0
- package/dist/src/ai/navigator.js +13 -4
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +57 -5
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +52 -7
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/researcher/locators.js +2 -2
- package/dist/src/ai/researcher/pagination.js +1 -1
- package/dist/src/ai/researcher/research-result.js +2 -2
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/task-agent.d.ts +2 -0
- package/dist/src/ai/task-agent.js +3 -1
- package/dist/src/ai/tester.js +19 -15
- package/dist/src/ai/tools.d.ts +4 -3
- package/dist/src/ai/tools.js +35 -7
- package/dist/src/api/request-result.js +2 -1
- package/dist/src/command-handler.d.ts +1 -0
- package/dist/src/command-handler.js +24 -3
- package/dist/src/commands/base-command.d.ts +5 -0
- package/dist/src/commands/base-command.js +3 -0
- package/dist/src/commands/explore-command.d.ts +2 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/freesail-command.js +8 -2
- package/dist/src/commands/init-command.js +1 -1
- package/dist/src/commands/navigate-command.d.ts +2 -1
- package/dist/src/commands/navigate-command.js +6 -0
- package/dist/src/commands/plan-load-command.d.ts +2 -1
- package/dist/src/commands/plan-load-command.js +4 -0
- package/dist/src/commands/plans-command.d.ts +3 -9
- package/dist/src/commands/plans-command.js +11 -21
- package/dist/src/commands/rerun-command.d.ts +2 -1
- package/dist/src/commands/rerun-command.js +5 -1
- package/dist/src/commands/research-command.d.ts +2 -1
- package/dist/src/commands/research-command.js +6 -0
- package/dist/src/commands/test-command.d.ts +2 -1
- package/dist/src/commands/test-command.js +4 -1
- package/dist/src/components/Autocomplete.js +26 -12
- package/dist/src/components/InputReadline.js +10 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/experience-tracker.js +4 -3
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +8 -0
- package/dist/src/knowledge-tracker.js +1 -1
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +16 -0
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +54 -2
- package/dist/src/utils/aria-ref.js +1 -1
- package/dist/src/utils/logger.js +8 -2
- package/dist/src/utils/markdown-query.d.ts +1 -48
- package/dist/src/utils/markdown-query.js +1 -444
- package/dist/src/utils/mdq/cli.d.ts +6 -0
- package/dist/src/utils/mdq/cli.js +122 -0
- package/dist/src/utils/mdq/edit.d.ts +24 -0
- package/dist/src/utils/mdq/edit.js +147 -0
- package/dist/src/utils/mdq/query.d.ts +118 -0
- package/dist/src/utils/mdq/query.js +451 -0
- package/dist/src/utils/strings.d.ts +1 -0
- package/dist/src/utils/strings.js +7 -0
- package/dist/src/utils/test-files.d.ts +1 -0
- package/dist/src/utils/test-files.js +5 -2
- package/docs/api-testing/planning.md +1 -1
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
- package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
- package/package.json +3 -2
- package/src/ai/agent.ts +3 -1
- package/src/ai/judge-provider.ts +62 -0
- package/src/ai/judge-tool.ts +35 -0
- package/src/ai/judge.ts +75 -0
- package/src/ai/navigator.ts +15 -4
- package/src/ai/pilot.ts +58 -5
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +51 -7
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/pagination.ts +1 -1
- package/src/ai/researcher/research-result.ts +2 -2
- package/src/ai/researcher.ts +1 -1
- package/src/ai/task-agent.ts +4 -1
- package/src/ai/tester.ts +19 -16
- package/src/ai/tools.ts +42 -7
- package/src/api/request-result.ts +2 -1
- package/src/command-handler.ts +28 -3
- package/src/commands/base-command.ts +9 -0
- package/src/commands/explore-command.ts +15 -2
- package/src/commands/freesail-command.ts +9 -2
- package/src/commands/init-command.ts +1 -1
- package/src/commands/navigate-command.ts +8 -1
- package/src/commands/plan-load-command.ts +6 -1
- package/src/commands/plans-command.ts +13 -29
- package/src/commands/rerun-command.ts +7 -2
- package/src/commands/research-command.ts +8 -1
- package/src/commands/test-command.ts +6 -2
- package/src/components/Autocomplete.tsx +39 -10
- package/src/components/InputReadline.tsx +10 -1
- package/src/config.ts +1 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/knowledge-tracker.ts +1 -1
- package/src/state-manager.ts +16 -0
- package/src/test-plan.ts +67 -2
- package/src/utils/aria-ref.ts +1 -1
- package/src/utils/logger.ts +6 -1
- package/src/utils/markdown-query.ts +1 -519
- package/src/utils/mdq/cli.ts +118 -0
- package/src/utils/mdq/edit.ts +158 -0
- package/src/utils/mdq/query.ts +556 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AIConfig } from '../config.js';
|
|
2
|
+
import { JudgeProvider } from './judge-provider.js';
|
|
3
|
+
export declare const UNDECIDED = "undecided";
|
|
4
|
+
export declare const JUDGE_PAGE_CAP = 12000;
|
|
5
|
+
export declare class Decision {
|
|
6
|
+
readonly value: string | null;
|
|
7
|
+
readonly confidence: number;
|
|
8
|
+
constructor(value: string | null, confidence: number);
|
|
9
|
+
get approved(): boolean;
|
|
10
|
+
get rejected(): boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class Judge {
|
|
13
|
+
provider: JudgeProvider;
|
|
14
|
+
enabled: {
|
|
15
|
+
tool: boolean;
|
|
16
|
+
direct: boolean;
|
|
17
|
+
};
|
|
18
|
+
constructor(provider: JudgeProvider, enabled: {
|
|
19
|
+
tool: boolean;
|
|
20
|
+
direct: boolean;
|
|
21
|
+
});
|
|
22
|
+
static fromConfig(config: AIConfig['decisionModel']): Judge | null;
|
|
23
|
+
get toolEnabled(): boolean;
|
|
24
|
+
decide(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
|
|
25
|
+
consult(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
|
|
26
|
+
request(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
|
|
27
|
+
recordFailure(error: unknown): null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { clearActivity, setActivity } from "../activity.js";
|
|
2
|
+
import { Observability } from "../observability.js";
|
|
3
|
+
import { createDebug } from "../utils/logger.js";
|
|
4
|
+
import { JudgeProvider } from "./judge-provider.js";
|
|
5
|
+
const debugLog = createDebug('explorbot:judge');
|
|
6
|
+
const APPROVAL_THRESHOLD = 0.7;
|
|
7
|
+
export const UNDECIDED = 'undecided';
|
|
8
|
+
export const JUDGE_PAGE_CAP = 12000;
|
|
9
|
+
export class Decision {
|
|
10
|
+
value;
|
|
11
|
+
confidence;
|
|
12
|
+
constructor(value, confidence) {
|
|
13
|
+
this.value = value;
|
|
14
|
+
this.confidence = confidence;
|
|
15
|
+
}
|
|
16
|
+
get approved() {
|
|
17
|
+
return this.value !== null;
|
|
18
|
+
}
|
|
19
|
+
get rejected() {
|
|
20
|
+
return this.value === null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class Judge {
|
|
24
|
+
provider;
|
|
25
|
+
enabled;
|
|
26
|
+
constructor(provider, enabled) {
|
|
27
|
+
this.provider = provider;
|
|
28
|
+
this.enabled = enabled;
|
|
29
|
+
}
|
|
30
|
+
static fromConfig(config) {
|
|
31
|
+
if (!config)
|
|
32
|
+
return null;
|
|
33
|
+
return new Judge(new JudgeProvider(config.provider, config.model), { tool: config.tool !== false, direct: config.direct !== false });
|
|
34
|
+
}
|
|
35
|
+
get toolEnabled() {
|
|
36
|
+
return this.enabled.tool;
|
|
37
|
+
}
|
|
38
|
+
async decide(question, options, state) {
|
|
39
|
+
if (!this.enabled.direct)
|
|
40
|
+
return new Decision(null, 0);
|
|
41
|
+
return this.consult(question, options, state);
|
|
42
|
+
}
|
|
43
|
+
async consult(question, options, state) {
|
|
44
|
+
if (Array.isArray(options) && options.length < 2)
|
|
45
|
+
return new Decision(null, 0);
|
|
46
|
+
return Observability.run('judge.decide', { tags: ['judge'] }, async () => {
|
|
47
|
+
setActivity('⚖️ Asking judge...', 'ai');
|
|
48
|
+
const decision = await this.request(question, options, state).finally(() => clearActivity());
|
|
49
|
+
Observability.getSpan()?.setAttribute('ai.telemetry.metadata.judgeDecision', JSON.stringify({ question, value: decision.value, confidence: decision.confidence }));
|
|
50
|
+
return decision;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async request(question, options, state) {
|
|
54
|
+
let list;
|
|
55
|
+
if (Array.isArray(options))
|
|
56
|
+
list = options;
|
|
57
|
+
const answer = await this.provider.decide(state, question, list).catch((error) => this.recordFailure(error));
|
|
58
|
+
if (!answer)
|
|
59
|
+
return new Decision(null, 0);
|
|
60
|
+
if (answer.probability <= APPROVAL_THRESHOLD)
|
|
61
|
+
return new Decision(null, answer.probability);
|
|
62
|
+
if (answer.value === UNDECIDED)
|
|
63
|
+
return new Decision(null, answer.probability);
|
|
64
|
+
return new Decision(answer.value, answer.probability);
|
|
65
|
+
}
|
|
66
|
+
recordFailure(error) {
|
|
67
|
+
debugLog('judge declined: %s', error);
|
|
68
|
+
Observability.getSpan()?.setAttribute('ai.telemetry.metadata.judgeError', String(error));
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -8,6 +8,7 @@ import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
|
8
8
|
import { type StateManager } from '../state-manager.js';
|
|
9
9
|
import { HooksRunner } from '../utils/hooks-runner.js';
|
|
10
10
|
import type { Agent, AgentDeps } from './agent.js';
|
|
11
|
+
import { type Decision, type Judge } from './judge.js';
|
|
11
12
|
import type { Provider } from './provider.js';
|
|
12
13
|
declare class Navigator implements Agent {
|
|
13
14
|
emoji: string;
|
|
@@ -23,6 +24,7 @@ declare class Navigator implements Agent {
|
|
|
23
24
|
explorer: Explorer;
|
|
24
25
|
config: ExplorbotConfig;
|
|
25
26
|
stateManager: StateManager;
|
|
27
|
+
judge?: Judge;
|
|
26
28
|
constructor(deps: AgentDeps);
|
|
27
29
|
get verifyAttempts(): number;
|
|
28
30
|
get verifyTimeout(): number;
|
|
@@ -82,6 +84,7 @@ declare class Navigator implements Agent {
|
|
|
82
84
|
args: any[];
|
|
83
85
|
}>;
|
|
84
86
|
totalAttempted: number;
|
|
87
|
+
judged?: Decision;
|
|
85
88
|
}>;
|
|
86
89
|
checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean;
|
|
87
90
|
}
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -13,6 +13,7 @@ import { loop, pause } from '../utils/loop.js';
|
|
|
13
13
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
14
14
|
import { normalizeInlineText } from "../utils/strings.js";
|
|
15
15
|
import { extractStatePath, isSameHostFamily, matchesNavigationUrl } from '../utils/url-matcher.js';
|
|
16
|
+
import { JUDGE_PAGE_CAP, UNDECIDED } from "./judge.js";
|
|
16
17
|
import { Researcher } from "./researcher.js";
|
|
17
18
|
import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
|
|
18
19
|
import { isInteractive } from './task-agent.js';
|
|
@@ -67,6 +68,7 @@ class Navigator {
|
|
|
67
68
|
explorer;
|
|
68
69
|
config;
|
|
69
70
|
stateManager;
|
|
71
|
+
judge;
|
|
70
72
|
constructor(deps) {
|
|
71
73
|
this.provider = deps.ai;
|
|
72
74
|
this.explorer = deps.explorer;
|
|
@@ -75,6 +77,7 @@ class Navigator {
|
|
|
75
77
|
this.knowledgeTracker = deps.knowledgeTracker;
|
|
76
78
|
this.experienceTracker = deps.stateManager.getExperienceTracker();
|
|
77
79
|
this.hooksRunner = new HooksRunner(deps.explorer, deps.config);
|
|
80
|
+
this.judge = deps.judge;
|
|
78
81
|
}
|
|
79
82
|
get verifyAttempts() {
|
|
80
83
|
return this.config.ai?.agents?.navigator?.verifyAttempts ?? 3;
|
|
@@ -517,8 +520,7 @@ class Navigator {
|
|
|
517
520
|
if (!value)
|
|
518
521
|
return;
|
|
519
522
|
const normalized = normalizeUrl(value);
|
|
520
|
-
|
|
521
|
-
visitCounts.set(normalized, (visitCounts.get(normalized) || 0) + 1);
|
|
523
|
+
visitCounts.set(normalized, (visitCounts.get(normalized) || 0) + 1);
|
|
522
524
|
};
|
|
523
525
|
for (const transition of history) {
|
|
524
526
|
countVisit(transition.toState.url);
|
|
@@ -527,7 +529,7 @@ class Navigator {
|
|
|
527
529
|
if (opts?.visitedUrls) {
|
|
528
530
|
for (const url of opts.visitedUrls) {
|
|
529
531
|
const normalized = normalizeUrl(url);
|
|
530
|
-
if (
|
|
532
|
+
if (!visitCounts.has(normalized)) {
|
|
531
533
|
visitCounts.set(normalized, 1);
|
|
532
534
|
}
|
|
533
535
|
}
|
|
@@ -619,6 +621,12 @@ class Navigator {
|
|
|
619
621
|
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
620
622
|
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
621
623
|
}
|
|
624
|
+
const verifiedClaims = Object.keys(actionResult.verifications ?? {}).filter((claim) => actionResult.getVerification(claim) === true);
|
|
625
|
+
const same = await this.judge?.decide('Which already verified claim means the same as the claim under consideration?', [...verifiedClaims, UNDECIDED], { claim: message });
|
|
626
|
+
if (same?.approved) {
|
|
627
|
+
tag('operation').log(`Judge matched claim to an already verified one: "${same.value}"`);
|
|
628
|
+
return { verified: true, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
629
|
+
}
|
|
622
630
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
623
631
|
let experience = '';
|
|
624
632
|
if (!actionResult.isInsideIframe) {
|
|
@@ -741,7 +749,8 @@ class Navigator {
|
|
|
741
749
|
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
742
750
|
if (inexpressible) {
|
|
743
751
|
tag('warning').log('No assertion could express this claim');
|
|
744
|
-
|
|
752
|
+
const judged = await this.judge?.decide('The page shows that this claim is true.', null, { claim: message, page: actionResult.getCompactARIA().slice(0, JUDGE_PAGE_CAP) });
|
|
753
|
+
return { verified: false, inexpressible, results, successfulCodes, assertionSteps, totalAttempted, judged };
|
|
745
754
|
}
|
|
746
755
|
actionResult.addVerification(message, verified);
|
|
747
756
|
this.stateManager.updateState(actionResult);
|
package/dist/src/ai/pilot.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type Test } from '../test-plan.js';
|
|
|
7
7
|
import type { Agent, AgentDeps } from './agent.js';
|
|
8
8
|
import type { Conversation } from './conversation.js';
|
|
9
9
|
import type { Fisherman } from './fisherman.js';
|
|
10
|
+
import { type Judge } from './judge.js';
|
|
10
11
|
import type { Navigator } from './navigator.js';
|
|
11
12
|
import type { Provider } from './provider.js';
|
|
12
13
|
import type { Researcher } from './researcher.js';
|
|
@@ -21,6 +22,7 @@ export declare class Pilot implements Agent {
|
|
|
21
22
|
requestStore: RequestStore;
|
|
22
23
|
playwrightRecorder: PlaywrightRecorder;
|
|
23
24
|
fisherman: Fisherman | null;
|
|
25
|
+
judge?: Judge;
|
|
24
26
|
constructor(deps: AgentDeps, agentTools: any, researcher: Researcher);
|
|
25
27
|
setFisherman(fisherman: Fisherman): void;
|
|
26
28
|
get stepsToReview(): number;
|
|
@@ -41,12 +43,14 @@ export declare class Pilot implements Agent {
|
|
|
41
43
|
reviewNewPage(task: Test, currentState: ActionResult, testerConversation: Conversation): Promise<string>;
|
|
42
44
|
analyzeProgress(task: Test, currentState: ActionResult, testerConversation: Conversation): Promise<string>;
|
|
43
45
|
settleExpectations(task: Test, finalState?: ActionResult): Promise<SettledExpectation[]>;
|
|
46
|
+
settleByJudge(task: Test, expectations: string[]): Promise<Map<string, SettledStatus>>;
|
|
44
47
|
formatExpectations(task: Test): string;
|
|
45
48
|
sendToPilot(userText: string, functionId: string, opts: {
|
|
46
49
|
tools?: boolean;
|
|
47
50
|
maxToolRoundtrips?: number;
|
|
48
51
|
task: Test;
|
|
49
52
|
}): Promise<string>;
|
|
53
|
+
announcePreparedData(text: string, task: Test, preparedCount: number): string;
|
|
50
54
|
getExperienceToc(): string;
|
|
51
55
|
pickPlanningTools(): Record<string, unknown>;
|
|
52
56
|
fishermanStatus(): string;
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -4,13 +4,14 @@ import { z } from 'zod';
|
|
|
4
4
|
import { ActionResult } from "../action-result.js";
|
|
5
5
|
import { ConfigParser } from "../config.js";
|
|
6
6
|
import { Stats } from "../stats.js";
|
|
7
|
-
import { TestResult } from "../test-plan.js";
|
|
7
|
+
import { TestResult, TestStatus } from "../test-plan.js";
|
|
8
8
|
import { collectInteractiveNodes } from "../utils/aria.js";
|
|
9
9
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
const debugLog = createDebug('explorbot:pilot');
|
|
12
12
|
import { truncateJson } from "../utils/strings.js";
|
|
13
13
|
import { createAskApiTool } from "./fisherman/tools.js";
|
|
14
|
+
import { UNDECIDED } from "./judge.js";
|
|
14
15
|
import { capabilityGroundingRule, dataProtectionRules } from "./rules.js";
|
|
15
16
|
import { isInteractive } from "./task-agent.js";
|
|
16
17
|
import { withdrawVisionTools } from "./tools.js";
|
|
@@ -21,6 +22,10 @@ const PILOT_REASONING_LIMIT = 500;
|
|
|
21
22
|
const PILOT_MESSAGE_LIMIT = 2;
|
|
22
23
|
const PILOT_MESSAGE_MAX_LENGTH = 160;
|
|
23
24
|
const PILOT_REQUEST_LIMIT = 5;
|
|
25
|
+
const OUTCOME_STATUS = {
|
|
26
|
+
'The run shows this outcome happened.': 'passed',
|
|
27
|
+
'The run shows this outcome did not happen.': 'failed',
|
|
28
|
+
};
|
|
24
29
|
export class Pilot {
|
|
25
30
|
emoji = '🧭';
|
|
26
31
|
provider;
|
|
@@ -32,6 +37,7 @@ export class Pilot {
|
|
|
32
37
|
requestStore;
|
|
33
38
|
playwrightRecorder;
|
|
34
39
|
fisherman = null;
|
|
40
|
+
judge;
|
|
35
41
|
constructor(deps, agentTools, researcher) {
|
|
36
42
|
this.provider = deps.ai;
|
|
37
43
|
this.agentTools = agentTools;
|
|
@@ -40,6 +46,7 @@ export class Pilot {
|
|
|
40
46
|
this.stateManager = deps.stateManager;
|
|
41
47
|
this.requestStore = deps.requestStore;
|
|
42
48
|
this.playwrightRecorder = deps.playwrightRecorder;
|
|
49
|
+
this.judge = deps.judge;
|
|
43
50
|
}
|
|
44
51
|
setFisherman(fisherman) {
|
|
45
52
|
this.fisherman = fisherman;
|
|
@@ -480,6 +487,9 @@ export class Pilot {
|
|
|
480
487
|
const toolCalls = testerConversation.getToolExecutions().slice(-this.stepsToReview);
|
|
481
488
|
const actionsContext = this.formatActions(toolCalls);
|
|
482
489
|
const stateContext = this.buildStateContext(currentState);
|
|
490
|
+
const healthy = await this.judge?.decide('The run is moving toward the goal and can continue without a supervisor reviewing it now.', null, { scenario: task.scenario, state: stateContext, recentActions: actionsContext });
|
|
491
|
+
if (healthy?.approved)
|
|
492
|
+
return '';
|
|
483
493
|
const hasFailures = toolCalls.length === 0 || toolCalls.some((t) => !t.wasSuccessful);
|
|
484
494
|
const text = await this.sendToPilot(dedent `
|
|
485
495
|
START URL: ${task.startUrl}
|
|
@@ -516,8 +526,12 @@ export class Pilot {
|
|
|
516
526
|
let undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
|
|
517
527
|
if (image)
|
|
518
528
|
undecided = task.expected;
|
|
529
|
+
let settledByJudge = new Map();
|
|
530
|
+
if (!image)
|
|
531
|
+
settledByJudge = await this.settleByJudge(task, undecided);
|
|
532
|
+
undecided = undecided.filter((text) => !settledByJudge.has(text));
|
|
519
533
|
if (!undecided.length)
|
|
520
|
-
return task.expected.map((text) => ({ text, status: decided(text) }));
|
|
534
|
+
return task.expected.map((text) => ({ text, status: settledByJudge.get(text) || decided(text) }));
|
|
521
535
|
const schema = z.object({
|
|
522
536
|
outcomes: z.array(z.object({
|
|
523
537
|
expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
|
|
@@ -581,6 +595,9 @@ export class Pilot {
|
|
|
581
595
|
response = await settle(userContent, this.provider.getAgenticModel('pilot'));
|
|
582
596
|
const judged = new Map((response?.object?.outcomes || []).map((outcome) => [outcome.expectation, outcome]));
|
|
583
597
|
return task.expected.map((text) => {
|
|
598
|
+
const byJudge = settledByJudge.get(text);
|
|
599
|
+
if (byJudge)
|
|
600
|
+
return { text, status: byJudge };
|
|
584
601
|
if (!undecided.includes(text))
|
|
585
602
|
return { text, status: decided(text) };
|
|
586
603
|
const outcome = judged.get(text);
|
|
@@ -589,6 +606,20 @@ export class Pilot {
|
|
|
589
606
|
return { text, status: outcome.status || 'unverified', evidence: outcome.evidence };
|
|
590
607
|
});
|
|
591
608
|
}
|
|
609
|
+
async settleByJudge(task, expectations) {
|
|
610
|
+
const settled = new Map();
|
|
611
|
+
const judge = this.judge;
|
|
612
|
+
if (!judge)
|
|
613
|
+
return settled;
|
|
614
|
+
const state = { scenario: task.scenario, runLog: task.notesToString() || 'No steps recorded.' };
|
|
615
|
+
await Promise.all(expectations.map(async (text) => {
|
|
616
|
+
const decision = await judge.decide(`What did this run establish about the expected outcome: ${text}`, [...Object.keys(OUTCOME_STATUS), UNDECIDED], state);
|
|
617
|
+
const status = OUTCOME_STATUS[decision.value ?? ''];
|
|
618
|
+
if (status)
|
|
619
|
+
settled.set(text, status);
|
|
620
|
+
}));
|
|
621
|
+
return settled;
|
|
622
|
+
}
|
|
592
623
|
formatExpectations(task) {
|
|
593
624
|
const checked = task.getCheckedExpectations();
|
|
594
625
|
const remaining = task.getRemainingExpectations();
|
|
@@ -605,6 +636,7 @@ export class Pilot {
|
|
|
605
636
|
}
|
|
606
637
|
this.conversation.addUserText(finalUserText);
|
|
607
638
|
const tools = { ...this.pickPlanningTools(), ...this.buildFishermanTools(opts.task) };
|
|
639
|
+
const preparedCount = opts.task.preparedData.length;
|
|
608
640
|
const result = await this.provider.invokeConversation(this.conversation, tools, {
|
|
609
641
|
maxToolRoundtrips: opts.maxToolRoundtrips ?? 0,
|
|
610
642
|
toolChoice: opts.tools ? 'auto' : 'none',
|
|
@@ -612,7 +644,7 @@ export class Pilot {
|
|
|
612
644
|
stopWhen: () => opts.task.hasFinished,
|
|
613
645
|
telemetry: { functionId },
|
|
614
646
|
});
|
|
615
|
-
const text = result?.response?.text || '';
|
|
647
|
+
const text = this.announcePreparedData(result?.response?.text || '', opts.task, preparedCount);
|
|
616
648
|
const learned = (result?.toolExecutions || []).filter((e) => e.toolName === 'learnExperience' && e.output?.content).map((e) => ({ url: e.output.url, content: e.output.content }));
|
|
617
649
|
if (learned.length === 0)
|
|
618
650
|
return text;
|
|
@@ -628,6 +660,23 @@ export class Pilot {
|
|
|
628
660
|
</applied_experience>
|
|
629
661
|
`;
|
|
630
662
|
}
|
|
663
|
+
announcePreparedData(text, task, preparedCount) {
|
|
664
|
+
const prepared = task.preparedData.slice(preparedCount);
|
|
665
|
+
if (prepared.length === 0)
|
|
666
|
+
return text;
|
|
667
|
+
let refresh = '';
|
|
668
|
+
if (task.status === TestStatus.IN_PROGRESS)
|
|
669
|
+
refresh = 'It was created after the page loaded, so the page does not show it yet. Run I.refreshPage() through form() before looking for it.';
|
|
670
|
+
return dedent `
|
|
671
|
+
${text}
|
|
672
|
+
|
|
673
|
+
<prepared_data>
|
|
674
|
+
Pilot created this data through the API for this test. Use it instead of creating the same data through the UI:
|
|
675
|
+
${prepared.map((item) => `- ${item}`).join('\n')}
|
|
676
|
+
${refresh}
|
|
677
|
+
</prepared_data>
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
631
680
|
getExperienceToc() {
|
|
632
681
|
const state = this.stateManager.getCurrentState();
|
|
633
682
|
if (!state)
|
|
@@ -635,7 +684,7 @@ export class Pilot {
|
|
|
635
684
|
return this.stateManager.getExperienceTracker().renderExperienceTocFor(ActionResult.fromState(state));
|
|
636
685
|
}
|
|
637
686
|
pickPlanningTools() {
|
|
638
|
-
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser } = this.agentTools ?? {};
|
|
687
|
+
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser, judge } = this.agentTools ?? {};
|
|
639
688
|
const planning = {};
|
|
640
689
|
if (see)
|
|
641
690
|
planning.see = see;
|
|
@@ -653,6 +702,8 @@ export class Pilot {
|
|
|
653
702
|
planning.learnExperience = learnExperience;
|
|
654
703
|
if (askUser)
|
|
655
704
|
planning.askUser = askUser;
|
|
705
|
+
if (judge)
|
|
706
|
+
planning.judge = judge;
|
|
656
707
|
withdrawVisionTools(planning);
|
|
657
708
|
return planning;
|
|
658
709
|
}
|
|
@@ -700,6 +751,7 @@ export class Pilot {
|
|
|
700
751
|
});
|
|
701
752
|
const stepText = `Precondition: created ${items.join(', ')}`;
|
|
702
753
|
task.addStep(stepText);
|
|
754
|
+
task.preparedData.push(...items);
|
|
703
755
|
tag('success').log(stepText);
|
|
704
756
|
return { noted: true, prepared: true, created: result.created };
|
|
705
757
|
},
|
|
@@ -1053,7 +1105,7 @@ export class Pilot {
|
|
|
1053
1105
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1054
1106
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1055
1107
|
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1056
|
-
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.
|
|
1108
|
+
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.refreshPage() through form.
|
|
1057
1109
|
|
|
1058
1110
|
${capabilityGroundingRule}
|
|
1059
1111
|
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -263,13 +263,14 @@ export class Planner extends PlannerBase {
|
|
|
263
263
|
.replaceEach((section) => {
|
|
264
264
|
const heading = section.query('heading').text().trim();
|
|
265
265
|
const withoutHeadings = mdq(section.text()).query('heading').replace('');
|
|
266
|
-
const body = mdq(withoutHeadings).query('hr').replace('').trim();
|
|
266
|
+
const body = mdq(withoutHeadings).query('hr').replace('').toString().trim();
|
|
267
267
|
if (body && !seenTitles.has(heading)) {
|
|
268
268
|
seenTitles.add(heading);
|
|
269
269
|
return section.text();
|
|
270
270
|
}
|
|
271
271
|
return '';
|
|
272
|
-
})
|
|
272
|
+
})
|
|
273
|
+
.toString();
|
|
273
274
|
}
|
|
274
275
|
const trimmedTitles = new Set();
|
|
275
276
|
for (const selector of ['section2', 'section3']) {
|
|
@@ -282,10 +283,11 @@ export class Planner extends PlannerBase {
|
|
|
282
283
|
const count = section.query('blockquote').count();
|
|
283
284
|
if (count <= 10)
|
|
284
285
|
return section.text();
|
|
285
|
-
const kept = mdq(section.text()).query('blockquote[10:]').replace('');
|
|
286
|
+
const kept = mdq(section.text()).query('blockquote[10:]').replace('').toString();
|
|
286
287
|
trimmedTitles.add(heading);
|
|
287
288
|
return `${kept.trimEnd()}\n> ... and ${count - 10} more discoveries\n`;
|
|
288
|
-
})
|
|
289
|
+
})
|
|
290
|
+
.toString();
|
|
289
291
|
}
|
|
290
292
|
return result.trim() || null;
|
|
291
293
|
}
|
|
@@ -360,7 +362,7 @@ export class Planner extends PlannerBase {
|
|
|
360
362
|
if (this.scout && this.docsWeight > 0) {
|
|
361
363
|
docsPromise = this.scout.collectDocs({ url: state.url, title: state.title, feature, excludeUrls: this.knowledgeTracker.applicationSpecUrls(state) });
|
|
362
364
|
}
|
|
363
|
-
let plannerResearch = mdq(research).query('code').replace('');
|
|
365
|
+
let plannerResearch = mdq(research).query('code').replace('').toString();
|
|
364
366
|
plannerResearch = mdq(plannerResearch)
|
|
365
367
|
.query('table')
|
|
366
368
|
.replaceEach((table) => {
|
|
@@ -372,7 +374,8 @@ export class Planner extends PlannerBase {
|
|
|
372
374
|
Type: r.Type || '',
|
|
373
375
|
}));
|
|
374
376
|
return jsonToTable(elementWithType, ['Element', 'Type']);
|
|
375
|
-
})
|
|
377
|
+
})
|
|
378
|
+
.toString();
|
|
376
379
|
const hasFocusedOverlay = hasFocusedSection(plannerResearch);
|
|
377
380
|
const focusNote = hasFocusedOverlay ? "IMPORTANT: One section is marked as **Focused** — this is the user's current focus area. Concentrate testing on the Focused section FIRST — test all interactions inside it before planning tests for the rest of the page." : '';
|
|
378
381
|
const featureFilter = feature ? `FOCUS FILTER: Only propose scenarios using elements relevant to "${feature}". Ignore all other elements.` : '';
|
|
@@ -32,8 +32,11 @@ export declare class Provider {
|
|
|
32
32
|
finalizeConfig(config: Record<string, any>, options: any, telemetry: any): void;
|
|
33
33
|
buildGenerateConfig(defaults: Record<string, any>, overrides: Record<string, any>, options: any): Record<string, any>;
|
|
34
34
|
recordUsage(agentName: string, modelName: string, usage: any): void;
|
|
35
|
-
raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number
|
|
35
|
+
raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number, busy?: {
|
|
36
|
+
tools: number;
|
|
37
|
+
}): Promise<T>;
|
|
36
38
|
recoverFromContextLength(error: any, messages: ModelMessage[], options: any, retry: (messages: ModelMessage[], options: any) => Promise<any>): Promise<any>;
|
|
39
|
+
recoverWithPlainJson(messages: ModelMessage[], schema: any, model: any, options: any): Promise<any>;
|
|
37
40
|
initLangfuse(): void;
|
|
38
41
|
getTelemetry(options: any): any;
|
|
39
42
|
startConversation(systemMessage: string, agentName?: string, model?: any): Conversation;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -3,7 +3,7 @@ import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
|
3
3
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
4
4
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
5
|
import dedent from 'dedent';
|
|
6
|
-
import { APICallError, generateObject, generateText, isStepCount, registerTelemetry, tool } from 'ai';
|
|
6
|
+
import { APICallError, NoObjectGeneratedError, asSchema, extractJsonMiddleware, generateObject, generateText, isStepCount, parsePartialJson, registerTelemetry, tool, wrapLanguageModel } from 'ai';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { clearActivity, setActivity } from "../activity.js";
|
|
9
9
|
import { configuredModels, modelName as getModelName } from '../config.js';
|
|
@@ -51,12 +51,12 @@ const CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum
|
|
|
51
51
|
function extractCachedTokens(usage) {
|
|
52
52
|
return usage?.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
53
53
|
}
|
|
54
|
-
function abortAfterIdle(ms, cancel, controller) {
|
|
54
|
+
function abortAfterIdle(ms, cancel, controller, busy) {
|
|
55
55
|
return new Promise((_, reject) => {
|
|
56
56
|
const tick = () => {
|
|
57
57
|
if (cancel.cancelled)
|
|
58
58
|
return;
|
|
59
|
-
if (executionController.isAwaitingInput()) {
|
|
59
|
+
if (executionController.isAwaitingInput() || busy.tools > 0) {
|
|
60
60
|
setTimeout(tick, ms);
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
@@ -78,7 +78,7 @@ export class Provider {
|
|
|
78
78
|
otelSdk = null;
|
|
79
79
|
defaultRetryOptions = {
|
|
80
80
|
maxAttempts: 3,
|
|
81
|
-
baseDelay:
|
|
81
|
+
baseDelay: 1000,
|
|
82
82
|
maxDelay: 10000,
|
|
83
83
|
retryCondition: (error) => {
|
|
84
84
|
return ((error.name === 'AI_APICallError' ||
|
|
@@ -231,12 +231,12 @@ export class Provider {
|
|
|
231
231
|
cached: extractCachedTokens(usage),
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
|
-
async raceWithIdleTimeout(fn, timeoutMs) {
|
|
234
|
+
async raceWithIdleTimeout(fn, timeoutMs, busy = { tools: 0 }) {
|
|
235
235
|
const cancel = { cancelled: false };
|
|
236
236
|
const controller = new AbortController();
|
|
237
237
|
const combinedSignal = combinedAbortSignal(controller);
|
|
238
238
|
try {
|
|
239
|
-
return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller)]);
|
|
239
|
+
return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller, busy)]);
|
|
240
240
|
}
|
|
241
241
|
finally {
|
|
242
242
|
cancel.cancelled = true;
|
|
@@ -249,6 +249,23 @@ export class Provider {
|
|
|
249
249
|
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
250
250
|
return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
251
251
|
}
|
|
252
|
+
async recoverWithPlainJson(messages, schema, model, options) {
|
|
253
|
+
const target = asSchema(schema);
|
|
254
|
+
tag('warning').log(`${getModelName(model)} returned no structured output, asking for plain JSON instead`);
|
|
255
|
+
const instruction = dedent `
|
|
256
|
+
Respond with a single JSON object that matches this JSON Schema, and nothing else:
|
|
257
|
+
${JSON.stringify(await target.jsonSchema)}
|
|
258
|
+
`;
|
|
259
|
+
const response = await this.chat([...messages, { role: 'user', content: instruction }], wrapLanguageModel({ model, middleware: extractJsonMiddleware() }), options);
|
|
260
|
+
const parsed = await parsePartialJson(response.text);
|
|
261
|
+
if (parsed.state !== 'successful-parse')
|
|
262
|
+
throw new AiError('No object generated: plain JSON fallback returned no parsable JSON');
|
|
263
|
+
const validated = await target.validate?.(parsed.value);
|
|
264
|
+
if (validated && !validated.success)
|
|
265
|
+
throw new AiError(`No object generated: plain JSON fallback did not match the schema: ${validated.error.message}`);
|
|
266
|
+
responseLog(parsed.value);
|
|
267
|
+
return { ...response, object: validated?.value ?? parsed.value };
|
|
268
|
+
}
|
|
252
269
|
initLangfuse() {
|
|
253
270
|
const { enabled, publicKey, secretKey, baseUrl } = this.config.langfuse || {};
|
|
254
271
|
if (!enabled || !publicKey || !secretKey) {
|
|
@@ -369,6 +386,8 @@ export class Provider {
|
|
|
369
386
|
}
|
|
370
387
|
async generateWithTools(messages, model, tools, options = {}) {
|
|
371
388
|
const modelName = getModelName(model);
|
|
389
|
+
const busy = { tools: 0 };
|
|
390
|
+
tools = withIdleExemption(tools, busy);
|
|
372
391
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
373
392
|
promptLog(`Using model: ${modelName}`);
|
|
374
393
|
let toolsWithCommentary = tools;
|
|
@@ -394,7 +413,7 @@ export class Provider {
|
|
|
394
413
|
const onStepEnd = (step) => {
|
|
395
414
|
stepMessages.push(...(step.response?.messages || []));
|
|
396
415
|
};
|
|
397
|
-
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages: attemptMessages, ...config, abortSignal: signal, onStepEnd }), config.timeout || 30000).catch((error) => {
|
|
416
|
+
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages: attemptMessages, ...config, abortSignal: signal, onStepEnd }), config.timeout || 30000, busy).catch((error) => {
|
|
398
417
|
if (stepMessages.length > 0) {
|
|
399
418
|
tag('warning').log(`Keeping ${stepMessages.length} messages from tool steps that already ran before the failure`);
|
|
400
419
|
executedStepMessages.push(...stepMessages);
|
|
@@ -476,6 +495,8 @@ export class Provider {
|
|
|
476
495
|
if (Provider.isContextLengthError(error)) {
|
|
477
496
|
return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateObject(m, schema, model, o));
|
|
478
497
|
}
|
|
498
|
+
if (NoObjectGeneratedError.isInstance(error))
|
|
499
|
+
return this.recoverWithPlainJson(messages, schema, modelToUse, options);
|
|
479
500
|
throw new AiError(error.message || error.toString());
|
|
480
501
|
}
|
|
481
502
|
}
|
|
@@ -695,4 +716,28 @@ function repairHarmonyChannel({ toolCall, tools }) {
|
|
|
695
716
|
tag('warning').log(`Repaired tool name '${toolCall.toolName}' → 'commentary'`);
|
|
696
717
|
return { ...toolCall, toolName: NARRATION_TOOL, input };
|
|
697
718
|
}
|
|
719
|
+
function withIdleExemption(tools, busy) {
|
|
720
|
+
if (!tools)
|
|
721
|
+
return tools;
|
|
722
|
+
const wrapped = {};
|
|
723
|
+
for (const [name, definition] of Object.entries(tools)) {
|
|
724
|
+
if (typeof definition?.execute !== 'function') {
|
|
725
|
+
wrapped[name] = definition;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
wrapped[name] = {
|
|
729
|
+
...definition,
|
|
730
|
+
execute: async (...args) => {
|
|
731
|
+
busy.tools++;
|
|
732
|
+
try {
|
|
733
|
+
return await definition.execute(...args);
|
|
734
|
+
}
|
|
735
|
+
finally {
|
|
736
|
+
busy.tools--;
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return wrapped;
|
|
742
|
+
}
|
|
698
743
|
export { AiError, Provider as AIProvider };
|
|
@@ -94,7 +94,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
94
94
|
let updated;
|
|
95
95
|
if (extQuery.count() > 0) {
|
|
96
96
|
const existing = extQuery.text().trimEnd();
|
|
97
|
-
updated = extQuery.replace(`${existing}\n\n${sectionMarkdown}\n`);
|
|
97
|
+
updated = extQuery.replace(`${existing}\n\n${sectionMarkdown}\n`).toString();
|
|
98
98
|
}
|
|
99
99
|
else {
|
|
100
100
|
updated = `${cached.trimEnd()}\n\n# Extended Research\n\n${sectionMarkdown}\n`;
|
|
@@ -481,7 +481,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
481
481
|
heading = mdq(sectionMarkdown).query('h2[0]');
|
|
482
482
|
if (heading.count() === 0)
|
|
483
483
|
return sectionMarkdown;
|
|
484
|
-
return heading.replace(`${heading.text().trimEnd()}\n\n> Container: '${containerCss}'\n\n`);
|
|
484
|
+
return heading.replace(`${heading.text().trimEnd()}\n\n> Container: '${containerCss}'\n\n`).toString();
|
|
485
485
|
}
|
|
486
486
|
_deduplicateExpandedSections(sections) {
|
|
487
487
|
const seen = new Set();
|
|
@@ -281,10 +281,10 @@ export function WithLocators(Base) {
|
|
|
281
281
|
if (sectionQuery.count() === 0)
|
|
282
282
|
sectionQuery = mdq(result.text).query(`section3(~"${escaped}")`);
|
|
283
283
|
if (newCss) {
|
|
284
|
-
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Container', `'${newCss}'`);
|
|
284
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Container', `'${newCss}'`).toString();
|
|
285
285
|
}
|
|
286
286
|
else {
|
|
287
|
-
result.text = sectionQuery.query('blockquote[0]').replace('');
|
|
287
|
+
result.text = sectionQuery.query('blockquote[0]').replace('').toString();
|
|
288
288
|
result.text = result.text.replace(`${FOCUSED_MARKER}\n`, '');
|
|
289
289
|
}
|
|
290
290
|
for (const loc of result.locators) {
|
|
@@ -56,7 +56,7 @@ export function WithPagination(Base) {
|
|
|
56
56
|
sectionQuery = mdq(result.text).query(`section3(~"${escaped}")`);
|
|
57
57
|
if (sectionQuery.count() === 0)
|
|
58
58
|
return;
|
|
59
|
-
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Pagination', strategy);
|
|
59
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Pagination', strategy).toString();
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
62
|
}
|
|
@@ -50,10 +50,10 @@ export class ResearchResult {
|
|
|
50
50
|
let sectionQuery = mdq(this.text).query(`section2(~"${escaped}")`);
|
|
51
51
|
if (sectionQuery.count() === 0)
|
|
52
52
|
sectionQuery = mdq(this.text).query(`section3(~"${escaped}")`);
|
|
53
|
-
const updated = sectionQuery.query('table').replace(`${newTable.trimEnd()}\n`);
|
|
53
|
+
const updated = sectionQuery.query('table').replace(`${newTable.trimEnd()}\n`).toString();
|
|
54
54
|
if (updated === this.text)
|
|
55
55
|
return;
|
|
56
|
-
section.rawMarkdown = mdq(section.rawMarkdown).query('table').replace(`${newTable.trimEnd()}\n`);
|
|
56
|
+
section.rawMarkdown = mdq(section.rawMarkdown).query('table').replace(`${newTable.trimEnd()}\n`).toString();
|
|
57
57
|
this.text = updated;
|
|
58
58
|
}
|
|
59
59
|
cleanup() {
|
|
@@ -261,7 +261,7 @@ export class Researcher extends ResearcherBase {
|
|
|
261
261
|
if (stateHash) {
|
|
262
262
|
researchFile = saveResearch(researchState, result.text, combinedHtml);
|
|
263
263
|
}
|
|
264
|
-
const summaryText = mdq(result.text).query('section2(/^summary/)').query('paragraph[0]').text().trim();
|
|
264
|
+
const summaryText = mdq(result.text).query('section2(/^summary/i)').query('paragraph[0]').text().trim();
|
|
265
265
|
const summaryLine = summaryText.split('\n')[0]?.trim().slice(0, 200);
|
|
266
266
|
if (summaryLine)
|
|
267
267
|
this.experienceTracker.updateSummary(this.actionResult, summaryLine);
|