testchimp-runner-core 0.0.39 → 0.0.41
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/execution-service.d.ts.map +1 -1
- package/dist/execution-service.js +1 -3
- package/dist/execution-service.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/decision-parser.d.ts.map +1 -1
- package/dist/orchestrator/decision-parser.js +16 -0
- package/dist/orchestrator/decision-parser.js.map +1 -1
- package/dist/orchestrator/index.d.ts +3 -1
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +8 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/orchestrator-agent.d.ts +10 -4
- package/dist/orchestrator/orchestrator-agent.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator-agent.js +347 -93
- package/dist/orchestrator/orchestrator-agent.js.map +1 -1
- package/dist/orchestrator/orchestrator-prompts.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator-prompts.js +364 -415
- package/dist/orchestrator/orchestrator-prompts.js.map +1 -1
- package/dist/orchestrator/page-loading-utils.d.ts +15 -0
- package/dist/orchestrator/page-loading-utils.d.ts.map +1 -0
- package/dist/orchestrator/page-loading-utils.js +115 -0
- package/dist/orchestrator/page-loading-utils.js.map +1 -0
- package/dist/orchestrator/page-som-handler.d.ts +2 -1
- package/dist/orchestrator/page-som-handler.d.ts.map +1 -1
- package/dist/orchestrator/page-som-handler.js +250 -33
- package/dist/orchestrator/page-som-handler.js.map +1 -1
- package/dist/orchestrator/site-learnings-utils.d.ts +31 -0
- package/dist/orchestrator/site-learnings-utils.d.ts.map +1 -0
- package/dist/orchestrator/site-learnings-utils.js +175 -0
- package/dist/orchestrator/site-learnings-utils.js.map +1 -0
- package/dist/orchestrator/som-types.d.ts +2 -0
- package/dist/orchestrator/som-types.d.ts.map +1 -1
- package/dist/orchestrator/som-types.js.map +1 -1
- package/dist/orchestrator/tools/take-screenshot.d.ts.map +1 -1
- package/dist/orchestrator/tools/take-screenshot.js +10 -1
- package/dist/orchestrator/tools/take-screenshot.js.map +1 -1
- package/dist/orchestrator/types.d.ts +54 -9
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/orchestrator/types.js.map +1 -1
- package/dist/progress-reporter.d.ts +23 -2
- package/dist/progress-reporter.d.ts.map +1 -1
- package/dist/progress-reporter.js.map +1 -1
- package/dist/scenario-service.d.ts +3 -3
- package/dist/scenario-service.d.ts.map +1 -1
- package/dist/scenario-service.js +6 -5
- package/dist/scenario-service.js.map +1 -1
- package/dist/scenario-worker-class.d.ts +7 -3
- package/dist/scenario-worker-class.d.ts.map +1 -1
- package/dist/scenario-worker-class.js +62 -9
- package/dist/scenario-worker-class.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/testing/agent-tester.d.ts +0 -35
- package/dist/testing/agent-tester.d.ts.map +0 -1
- package/dist/testing/agent-tester.js +0 -84
- package/dist/testing/agent-tester.js.map +0 -1
- package/dist/testing/ref-translator-tester.d.ts +0 -44
- package/dist/testing/ref-translator-tester.d.ts.map +0 -1
- package/dist/testing/ref-translator-tester.js +0 -104
- package/dist/testing/ref-translator-tester.js.map +0 -1
- package/dist/utils/hierarchical-selector.d.ts +0 -47
- package/dist/utils/hierarchical-selector.d.ts.map +0 -1
- package/dist/utils/hierarchical-selector.js +0 -212
- package/dist/utils/hierarchical-selector.js.map +0 -1
- package/dist/utils/ref-attacher.d.ts +0 -21
- package/dist/utils/ref-attacher.d.ts.map +0 -1
- package/dist/utils/ref-attacher.js +0 -149
- package/dist/utils/ref-attacher.js.map +0 -1
- package/dist/utils/ref-translator.d.ts +0 -49
- package/dist/utils/ref-translator.d.ts.map +0 -1
- package/dist/utils/ref-translator.js +0 -276
- package/dist/utils/ref-translator.js.map +0 -1
|
@@ -5,6 +5,110 @@
|
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.OrchestratorPrompts = void 0;
|
|
8
|
+
// ========== UTILITY FUNCTIONS ==========
|
|
9
|
+
/**
|
|
10
|
+
* Truncate URL to avoid bloating prompts
|
|
11
|
+
*/
|
|
12
|
+
function truncateUrl(url, maxLength = 300) {
|
|
13
|
+
if (!url || url.length <= maxLength)
|
|
14
|
+
return url;
|
|
15
|
+
return url.substring(0, maxLength) + '...';
|
|
16
|
+
}
|
|
17
|
+
// ========== SHARED PROMPT SECTIONS (to avoid duplication) ==========
|
|
18
|
+
const DISCRETE_EXPERIENCE_LOOP = `DISCRETE EXPERIENCE LOOP (YOU ARE STATELESS - NO SCREENSHOT MEMORY):
|
|
19
|
+
You operate in iterations: receive state → decide → sleep → wake with NEW state.
|
|
20
|
+
Each iteration you receive: current screenshot, past actions, collected memories, and noteToFutureSelf you wrote in last iteration.
|
|
21
|
+
CRITICAL: You do NOT see previous screenshots (unless you specifically request) - only text descriptions of past actions!
|
|
22
|
+
Write explicit EXPECTED STATE in noteToFutureSelf so your future self can verify against the future screenshot.
|
|
23
|
+
Example: "Clicked hamburger menu (was collapsed). EXPECT: menu expanded with 'settings' items visible"`;
|
|
24
|
+
const SITE_LEARNINGS_GUIDE = `SITE LEARNINGS: Build mental model (persistent across journeys)
|
|
25
|
+
|
|
26
|
+
NAMING (check SCREEN STATE VOCABULARY first):
|
|
27
|
+
- screen: REUSE from vocabulary ("login", "dashboard") or create if new. NEVER: "about:blank", "loading"
|
|
28
|
+
- state: INFER from COMPLETED STEPS (max 3 GENERIC dims - user role/context, NOT specific data)
|
|
29
|
+
Dimensions describe USER STATE (logged-in, admin, cart-empty), NOT data values (workspace names, usernames, products)
|
|
30
|
+
✅ "logged-in,admin", "guest,cart-empty"
|
|
31
|
+
❌ "testchimp-selected" (workspace name is data!), "user-john" (username is data!)
|
|
32
|
+
|
|
33
|
+
LEARNINGS (semantic insights that persist):
|
|
34
|
+
Focus on BEHAVIOR and PATTERNS that will help on future runs, when SoM IDs are completely different.
|
|
35
|
+
|
|
36
|
+
WHY NO SOM IDS: SoM markers (1, 2, [5], [6], element 9) regenerate EVERY page load - different numbers each time!
|
|
37
|
+
A learning with "element 9" is useless on next run when that same button is "element 3".
|
|
38
|
+
|
|
39
|
+
STORE: Non-obvious behavior, interaction quirks, selector strategies
|
|
40
|
+
✅ "Dropdown opens on caret icon click, not container div"
|
|
41
|
+
✅ "Delete requires overflow menu (not directly visible)"
|
|
42
|
+
✅ "Search triggers on Enter, not auto-search while typing"
|
|
43
|
+
|
|
44
|
+
DON'T STORE: Element catalogs, SoM IDs, obvious facts, attribute documentation
|
|
45
|
+
❌ "Continue with Google button" (element listing - adds no behavioral value)
|
|
46
|
+
❌ "opener is SoM id [6]" (ephemeral - will be different ID next run!)
|
|
47
|
+
❌ "input name=emailOrUsername" (documenting HTML - not useful)
|
|
48
|
+
|
|
49
|
+
Ask: "Will this help when SoM IDs are completely different?" NO → don't store
|
|
50
|
+
|
|
51
|
+
STEP COMPLETION: Check ALL signals (memory, URL, screenshot, noteToFutureSelf) vs step goal.
|
|
52
|
+
Process: Expected (from noteToSelf) → Actual (commands success? URL changed? content visible?) → Decide
|
|
53
|
+
- Commands ✓ + URL changed + expected page → COMPLETE
|
|
54
|
+
- Commands ✓ + error shown → CONTINUE (retry)
|
|
55
|
+
- Command failed → CONTINUE (different selector)
|
|
56
|
+
|
|
57
|
+
`;
|
|
58
|
+
const NOTETOSELF_GUIDE = `NOTETOSELF: Capture thinking/intentions + EXPLICIT EXPECTED STATE for verification.
|
|
59
|
+
✅ "Clicked menu. EXPECT: expanded with 'Settings' visible"
|
|
60
|
+
❌ "Click menu" (future can't verify!)
|
|
61
|
+
Include: strategy, backups if fails, what to verify next.`;
|
|
62
|
+
// Response schema - exact TypeScript interface the agent must follow
|
|
63
|
+
const RESPONSE_SCHEMA = `
|
|
64
|
+
RESPONSE FORMAT (exact TypeScript interface):
|
|
65
|
+
|
|
66
|
+
interface AgentDecision {
|
|
67
|
+
// Required fields
|
|
68
|
+
status: 'complete' | 'stuck' | 'infeasible' | 'continue';
|
|
69
|
+
statusReasoning: string;
|
|
70
|
+
reasoning: string;
|
|
71
|
+
|
|
72
|
+
// Screen identification (REQUIRED - always identify current screen)
|
|
73
|
+
screenState: {
|
|
74
|
+
screen: string; // Screen name - REUSE from SCREEN STATE KNOWLEDGE if possible
|
|
75
|
+
state: string; // State dimensions: "admin", "admin,empty-cart", "" for default
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Site learnings (OPTIONAL - only when learning something NEW/IMPORTANT)
|
|
79
|
+
siteLearningsUpdate?: {
|
|
80
|
+
screens?: {
|
|
81
|
+
[screenName: string]: {
|
|
82
|
+
states: {
|
|
83
|
+
[stateName: string]: {
|
|
84
|
+
observations?: Array<{ id?: number; text: string }>; // Add (no id) or Update (with id)
|
|
85
|
+
deleteObservationIds?: number[];
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
uxPatterns?: Array<{ id?: number; text: string }>; // Add (no id) or Update (with id)
|
|
91
|
+
deleteUxPatternIds?: number[];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Commands to execute
|
|
95
|
+
commands?: Array<SomCommand | string>;
|
|
96
|
+
commandReasoning?: string;
|
|
97
|
+
|
|
98
|
+
// Note to future self (your only memory continuity)
|
|
99
|
+
noteToFutureSelf?: string;
|
|
100
|
+
|
|
101
|
+
// Other optional fields
|
|
102
|
+
toolCalls?: Array<{ name: string; params: any }>;
|
|
103
|
+
toolReasoning?: string;
|
|
104
|
+
blockerDetected?: { description: string; clearingCommands: string[] };
|
|
105
|
+
memoryUpdate?: { action: string; observation: string; extractedData?: Record<string, any> };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
CRITICAL: uxPatterns array must have objects with BOTH id and text fields!
|
|
109
|
+
Example: { "id": 1, "text": "Pattern description" } or { "text": "New pattern" } (no id for new)
|
|
110
|
+
`;
|
|
111
|
+
// ===================================================================
|
|
8
112
|
class OrchestratorPrompts {
|
|
9
113
|
/**
|
|
10
114
|
* Build main system prompt for selector-based mode
|
|
@@ -12,108 +116,27 @@ class OrchestratorPrompts {
|
|
|
12
116
|
static buildSystemPrompt(toolDescriptions, enableCoordinateMode = false) {
|
|
13
117
|
return `You are an intelligent test automation agent that executes web scenarios using Playwright.
|
|
14
118
|
|
|
15
|
-
|
|
16
|
-
You operate in iterations: receive state → decide → sleep → wake with new state.
|
|
17
|
-
System waits for page stability after each batch. Note to future self: strategy, what to verify, backup plans.
|
|
18
|
-
|
|
19
|
-
COMMON UX PATTERNS (critical for navigation):
|
|
20
|
-
• Disabled buttons → Fill required fields first to enable them
|
|
21
|
-
• Missing SoM ID → Element likely disabled (fill prerequisites first)
|
|
22
|
-
• Modals/overlays → Dismiss or interact before underlying content
|
|
23
|
-
• Hover effects → Reveal tooltips/menus before clicking
|
|
24
|
-
• Dropdowns/autocomplete → Type then select from revealed options
|
|
25
|
-
• Toasts/alerts → Read for success/error feedback (may be transient)
|
|
26
|
-
• Tabs/steppers → Reveal new content in same page (not navigation)
|
|
27
|
-
• Form validation → Red highlights/borders = invalid, fix before submit
|
|
28
|
-
• Confirmation dialogs → Accept/dismiss before proceeding
|
|
29
|
-
• Lazy loading → Scroll down to load more content
|
|
30
|
-
• Accordions/expandable → Click header to toggle visibility
|
|
119
|
+
${DISCRETE_EXPERIENCE_LOOP}
|
|
31
120
|
|
|
32
|
-
|
|
33
|
-
1. ALWAYS prefer SoM-marked elements (they have reliable selectors)
|
|
34
|
-
2. If element not marked: try refresh_som_markers tool (may have just enabled)
|
|
35
|
-
3. Last resort: coordinate-based interaction (when element truly unmarked)
|
|
121
|
+
UX PATTERNS: Disabled buttons→fill prerequisites. Modals→dismiss first. Dropdowns→type then select. Form errors→fix before submit.
|
|
36
122
|
|
|
37
|
-
|
|
123
|
+
STRATEGY: Prefer SoM elements. If unmarked, try refresh_som_markers or coordinates.
|
|
38
124
|
|
|
39
125
|
${toolDescriptions}
|
|
40
126
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
**When to mark COMPLETE:**
|
|
45
|
-
- Step: "Navigate to URL" → Mark complete after navigate command succeeds (don't login yet!)
|
|
46
|
-
- Step: "Fill login form" → Mark complete after filling fields (don't click submit yet!)
|
|
47
|
-
- Step: "Click Submit" → Mark complete after clicking (don't wait for next page!)
|
|
48
|
-
|
|
49
|
-
**DO NOT:**
|
|
50
|
-
- Continue with future steps while still on current step
|
|
51
|
-
- Assume the step wants you to do more than stated
|
|
52
|
-
- Wait for side effects (navigation, etc.) before marking complete
|
|
53
|
-
|
|
54
|
-
**The goal text is LITERAL** - do exactly what it says, then mark complete.
|
|
55
|
-
|
|
56
|
-
OUTPUT FORMAT (JSON):
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
"status": "continue" | "complete" | "stuck" | "infeasible",
|
|
60
|
-
"reasoning": "your thinking",
|
|
61
|
-
|
|
62
|
-
"commands": [ // Mix ref and playwright commands as needed
|
|
63
|
-
{ "type": "playwright", "code": "await page.goto('https://example.com')" },
|
|
64
|
-
{ "type": "ref", "ref": "e22", "operation": "fill", "value": "text" },
|
|
65
|
-
{ "type": "ref", "ref": "e31", "operation": "click" },
|
|
66
|
-
{ "type": "playwright", "code": "await page.waitForLoadState('networkidle')" }
|
|
67
|
-
],
|
|
68
|
-
|
|
69
|
-
"toolCalls": [{ "name": "tool_name", "params": {} }],
|
|
70
|
-
"blockerDetected": { "description": "...", "clearingCommands": ["..."] },
|
|
71
|
-
"experiences": ["app pattern"],
|
|
72
|
-
"noteToFutureSelf": "See NOTETOSELF GUIDELINES below",
|
|
73
|
-
"debugInfo": { // OPTIONAL: Only if you have confident prompt improvement suggestions
|
|
74
|
-
"suggestedPromptUpdates": "Add instruction: When form has Country dropdown, select country BEFORE filling phone (enables country code)",
|
|
75
|
-
"reasoning": "Encountered this pattern 3 times - dropdown selection unlocks dependent fields"
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
NOTETOSELF: Your only cognition continuity - capture THINKING/INTENTIONS (history has actions).
|
|
80
|
-
Include: strategy, hypothesis, alternatives/backups if fails, what to verify next, observations.
|
|
81
|
-
Example: "Strategy: Clicking ID 1 for menu. Backup: try ID 2/3 or coord (8%,15%). Want to verify: menu expands with nav options."
|
|
82
|
-
|
|
83
|
-
META-LEARNING (debugInfo): Could this prompt have been better. Suggest fixes.
|
|
127
|
+
STEP COMPLETION: Mark "complete" when goal PROVABLY achieved (check current state, not memory).
|
|
128
|
+
Verify: URL match? Fields filled? Expected content visible? Compare vs noteToFutureSelf expectations.
|
|
129
|
+
Do ONLY what step asks - no extra actions.
|
|
84
130
|
|
|
85
|
-
|
|
86
|
-
RULES: Do only step goal. Minimal commands. Try different selectors if fail. Use blockerDetected for modals.
|
|
131
|
+
${RESPONSE_SCHEMA}
|
|
87
132
|
|
|
88
|
-
|
|
133
|
+
OUTPUT: Return valid JSON matching AgentDecision interface.
|
|
89
134
|
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
"commands": [
|
|
93
|
-
"await page.fill('input[name=\"email\"]', 'user@test.com')",
|
|
94
|
-
"await page.fill('input[name=\"password\"]', 'secret123')",
|
|
95
|
-
"await page.click('button[type=\"submit\"]')"
|
|
96
|
-
]
|
|
97
|
-
}
|
|
135
|
+
${SITE_LEARNINGS_GUIDE}
|
|
98
136
|
|
|
99
|
-
|
|
100
|
-
1. getByRole: page.getByRole('button', {name: 'Login'})
|
|
101
|
-
2. getByLabel: page.getByLabel('Email address')
|
|
102
|
-
3. getByPlaceholder: page.getByPlaceholder('Enter email')
|
|
103
|
-
4. getByText: page.getByText('Sign in')
|
|
104
|
-
5. CSS: page.locator('input[name="email"]')
|
|
105
|
-
6. Test IDs: page.getByTestId('login-button')
|
|
106
|
-
|
|
107
|
-
Example login commands:
|
|
108
|
-
{
|
|
109
|
-
"commands": [
|
|
110
|
-
"await page.getByLabel('Email').fill('user@test.com')",
|
|
111
|
-
"await page.getByLabel('Password').fill('secret123')",
|
|
112
|
-
"await page.getByRole('button', {name: 'Submit'}).click()"
|
|
113
|
-
]
|
|
114
|
-
}
|
|
137
|
+
${NOTETOSELF_GUIDE}
|
|
115
138
|
|
|
116
|
-
|
|
139
|
+
STATUS: complete/continue/stuck/infeasible. RULES: Do step goal only. Try different selectors if fail.`;
|
|
117
140
|
}
|
|
118
141
|
/**
|
|
119
142
|
* Build SoM (Set-of-Marks) system prompt for visual element identification
|
|
@@ -132,30 +155,16 @@ Strong preference order:
|
|
|
132
155
|
If you use coordinates, you MUST explain in commandReasoning why no SoM-marked alternative exists.` : '';
|
|
133
156
|
return `You are an intelligent test automation agent using Set-of-Marks (SoM) visual element identification.${coordinateRestriction}
|
|
134
157
|
|
|
135
|
-
|
|
136
|
-
You operate in iterations: receive state → decide → sleep → wake with new state.
|
|
137
|
-
System waits for page stability after each batch.
|
|
158
|
+
${DISCRETE_EXPERIENCE_LOOP}
|
|
138
159
|
|
|
139
|
-
|
|
140
|
-
You have NO memory between iterations. Each "wake up" is like a fresh start - you only see:
|
|
141
|
-
- Current screenshot
|
|
142
|
-
- Current step goal
|
|
143
|
-
- Previous step descriptions
|
|
144
|
-
- Your noteToFutureSelf from last iteration
|
|
145
|
-
|
|
146
|
-
The noteToFutureSelf is your ONLY way to maintain a continuous stream of thinking across iterations. Use it strategically to:
|
|
147
|
-
• Document your current intentions and strategy
|
|
148
|
-
• Record what you were thinking/planning
|
|
149
|
-
• Give specific advice to your future self about what to look for
|
|
150
|
-
• Note any observations or patterns you've discovered
|
|
151
|
-
• Suggest backup plans if current approach fails
|
|
160
|
+
${NOTETOSELF_GUIDE}
|
|
152
161
|
|
|
153
162
|
IMPORTANT: You will receive a screenshot with COLOR-CODED BOUNDING BOXES and IDs overlaid on interactive elements.
|
|
154
163
|
|
|
155
164
|
SCREENSHOT SCOPE:
|
|
156
|
-
- Shows
|
|
157
|
-
-
|
|
158
|
-
-
|
|
165
|
+
- Shows FULL PAGE (entire scrollable content, including below-fold elements)
|
|
166
|
+
- ALL interactive elements across the entire page are marked with SoM IDs
|
|
167
|
+
- You can see and interact with any element on the page without scrolling
|
|
159
168
|
|
|
160
169
|
VISUAL MARKER SYSTEM:
|
|
161
170
|
- Each interactive element has a colored bounding box with a unique color
|
|
@@ -213,175 +222,27 @@ COMMANDS ARRAY: Mix actions (has 'action') and verifications (has 'verificationT
|
|
|
213
222
|
Example: [{"elementRef":"4","action":"fill","value":"Hello"}, {"elementRef":"3","verificationType":"textContains","expected":"You: Hello"}]
|
|
214
223
|
CRITICAL: Verification steps MUST generate verification commands (never 0 commands) - don't just visually confirm!
|
|
215
224
|
|
|
216
|
-
|
|
217
|
-
Use percentage-based coords for unmarked elements:
|
|
218
|
-
{ "action": "click", "coord": { "x": 85.625, "y": 12.375 } }
|
|
219
|
-
|
|
220
|
-
Format: percentages 0-100, MUST use 3 decimals (0.000 = top-left, 50.000 = center, 100.000 = bottom-right).
|
|
221
|
-
After coord click, magenta "clicked" marker appears. Use view_previous_screenshot tool to verify if result unexpected.
|
|
225
|
+
${RESPONSE_SCHEMA}
|
|
222
226
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// Available actions: click, doubleClick, rightClick, hover, drag, fill, press, select, check, uncheck, focus, blur, scroll, navigate, goBack, goForward, reload
|
|
228
|
-
// Available verifications: textContains, textEquals, valueEquals, valueEmpty, isVisible, isHidden, isEnabled, isDisabled, isChecked, isUnchecked, countEquals, countGreaterThan, countLessThan, hasClass, hasAttribute
|
|
229
|
-
|
|
230
|
-
interface AgentDecisionLLMResponse {
|
|
231
|
-
status: "continue" | "complete" | "stuck" | "infeasible";
|
|
232
|
-
reasoning: string;
|
|
233
|
-
commands?: (SomCommand | SomVerification)[]; // REPAIR MODE: Can be empty [] if step already done/obsolete
|
|
234
|
-
commandReasoning?: string;
|
|
235
|
-
toolCalls?: Array<{ name: string; params: Record<string, any> }>;
|
|
236
|
-
noteToFutureSelf?: string;
|
|
237
|
-
experiences?: string[];
|
|
238
|
-
blockerDetected?: { description: string; clearingCommands: SomCommand[] };
|
|
239
|
-
debugInfo?: { suggestedPromptUpdates?: string; reasoning?: string };
|
|
240
|
-
}
|
|
241
|
-
\`\`\`
|
|
242
|
-
|
|
243
|
-
NOTETOSELF: Your only continuity. Include: hypothesis, strategy, backup plans if fails, what to verify, observations.
|
|
244
|
-
Example: "Strategy: Click ID 1 for menu. Backup: try ID 2/3 or coord (8%,15%). Want to verify: menu expands."
|
|
245
|
-
|
|
246
|
-
EXAMPLE RESPONSES:
|
|
247
|
-
|
|
248
|
-
Action step:
|
|
249
|
-
\`\`\`json
|
|
250
|
-
{
|
|
251
|
-
"status": "continue",
|
|
252
|
-
"reasoning": "Need to fill login form with credentials",
|
|
253
|
-
"commands": [
|
|
254
|
-
{ "elementRef": "5", "action": "fill", "value": "user@example.com" },
|
|
255
|
-
{ "elementRef": "7", "action": "fill", "value": "password123" },
|
|
256
|
-
{ "elementRef": "12", "action": "click" }
|
|
257
|
-
],
|
|
258
|
-
"commandReasoning": "Filling email (ID 5), password (ID 7), clicking submit (ID 12)"
|
|
259
|
-
}
|
|
260
|
-
\`\`\`
|
|
261
|
-
|
|
262
|
-
Verification step:
|
|
263
|
-
\`\`\`json
|
|
264
|
-
{
|
|
265
|
-
"status": "complete",
|
|
266
|
-
"reasoning": "Message sent and verified in conversation",
|
|
267
|
-
"commands": [
|
|
268
|
-
{ "elementRef": "3", "verificationType": "textContains", "expected": "You: Hello", "description": "Message appears in thread" },
|
|
269
|
-
{ "elementRef": "4", "verificationType": "valueEmpty", "description": "Input cleared" }
|
|
270
|
-
],
|
|
271
|
-
"commandReasoning": "Verifying message visible in conversation (ID 3) and input empty (ID 4)"
|
|
272
|
-
}
|
|
273
|
-
\`\`\`
|
|
274
|
-
|
|
275
|
-
REPAIR MODE - Step already completed (DELETE case):
|
|
276
|
-
\`\`\`json
|
|
277
|
-
{
|
|
278
|
-
"status": "complete",
|
|
279
|
-
"reasoning": "Step asked to 'Dismiss welcome modal' but I see no modal in current screenshot - it was already dismissed by prior steps",
|
|
280
|
-
"commands": [],
|
|
281
|
-
"commandReasoning": "No commands needed - step goal already achieved/obsolete"
|
|
282
|
-
}
|
|
283
|
-
\`\`\`
|
|
227
|
+
COORDS: { "action": "click", "coord": { "x": 85.625, "y": 12.375 } }. Use 3 decimals, 0-100%.
|
|
228
|
+
NAVIGATE: { "action": "navigate", "value": "https://..." }
|
|
229
|
+
SCROLL: { "action": "scroll", "scrollDirection": "down", "scrollAmount": 500 }
|
|
230
|
+
PRESS: { "elementRef": "5", "action": "press", "value": "Enter" } (NO coord for press!)
|
|
284
231
|
|
|
285
|
-
OUTPUT
|
|
232
|
+
OUTPUT: Return valid JSON. Example: { "status": "complete", "commands": [{"elementRef":"5","action":"fill","value":"test"}], "screenState": {"screen":"login","state":""} }`;
|
|
286
233
|
}
|
|
287
234
|
/**
|
|
288
235
|
* Build coordinate-specific system prompt (used when selectors repeatedly fail)
|
|
289
236
|
*/
|
|
290
237
|
static buildCoordinateSystemPrompt() {
|
|
291
|
-
return `
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
You will see a screenshot with color-coded bounding boxes and ID labels attached to each element.
|
|
297
|
-
|
|
298
|
-
CRITICAL - IDENTIFY THE CORRECT ELEMENT:
|
|
299
|
-
1. READ the step goal carefully - what specific element are you looking for?
|
|
300
|
-
2. Look for the colored bounding box that matches the element description
|
|
301
|
-
3. The ID label is at TOP-RIGHT corner, ABOVE the box (bottom of label touches top of box)
|
|
302
|
-
4. Match the label color to the bounding box color
|
|
303
|
-
5. LOCATE that element in the screenshot (NOT a similar-looking element!)
|
|
304
|
-
6. VERIFY position using screen regions:
|
|
305
|
-
- Left sidebar/menu: xPercent ~5-25% (FAR LEFT)
|
|
306
|
-
- Center content: xPercent ~30-70%
|
|
307
|
-
- Right panel/sidebar: xPercent ~75-95% (FAR RIGHT)
|
|
308
|
-
7. CALCULATE percentages from element's CENTER position
|
|
309
|
-
8. SANITY CHECK your percentages:
|
|
310
|
-
- Sidebar menu item at 85%? WRONG - that's far right, not sidebar!
|
|
311
|
-
- Button in top-left at 90%? WRONG - that's top-right!
|
|
312
|
-
- Element description says "left" but x > 50%? WRONG - recheck!
|
|
313
|
-
|
|
314
|
-
Example thought process:
|
|
315
|
-
Goal: "Click Settings link in left navigation"
|
|
316
|
-
→ I see "Settings" text in LEFT navigation panel in the screenshot
|
|
317
|
-
→ Visual estimate: The link appears in the far left sidebar
|
|
318
|
-
→ Horizontal: The link center is roughly 1/8th from the left edge → ~12-13% from left
|
|
319
|
-
→ Vertical: The link center is roughly 1/3rd down from top → ~30-35% from top
|
|
320
|
-
→ xPercent: 12.500, yPercent: 32.000
|
|
321
|
-
→ Sanity check: 12.5% is FAR LEFT (NOT 80%+ which would be far right!)
|
|
322
|
-
→ Description: "Clicking center of Settings link in left sidebar"
|
|
323
|
-
|
|
324
|
-
CRITICAL VISUAL ESTIMATION TIPS:
|
|
325
|
-
- Divide screenshot mentally into quadrants/regions
|
|
326
|
-
- Left sidebar usually ~5-20% from left, center content ~30-70%, right sidebar ~75-95%
|
|
327
|
-
- Aim for CENTER of element, not edges
|
|
328
|
-
- Top bar usually 0-10% from top, footer usually 90-100%
|
|
329
|
-
- Be conservative: slightly off-center is better than way off
|
|
330
|
-
|
|
331
|
-
YOUR RESPONSE FORMAT - Output JSON matching this interface:
|
|
332
|
-
|
|
333
|
-
interface AgentDecisionLLMResponse {
|
|
334
|
-
status: string; // REQUIRED: "continue" (usually for coordinate mode)
|
|
335
|
-
reasoning: string; // REQUIRED: "I see [element] at (X%, Y%) - using coordinates"
|
|
336
|
-
coordinateAction: { // REQUIRED in coordinate mode
|
|
337
|
-
type: "coordinate";
|
|
338
|
-
action: "click" | "doubleClick" | "rightClick" | "hover" | "drag" | "fill" | "scroll";
|
|
339
|
-
xPercent: number; // 0-100, 3 decimals
|
|
340
|
-
yPercent: number; // 0-100, 3 decimals
|
|
341
|
-
toXPercent?: number; // For drag
|
|
342
|
-
toYPercent?: number; // For drag
|
|
343
|
-
value?: string; // For fill
|
|
344
|
-
scrollAmount?: number; // For scroll
|
|
345
|
-
};
|
|
346
|
-
noteToFutureSelf?: string; // Optional: What to try if this fails
|
|
347
|
-
}
|
|
238
|
+
return `Selectors FAILED. Use COORDINATE-BASED actions (visual estimation from screenshot).
|
|
239
|
+
|
|
240
|
+
COORD REFERENCE: Top-left=0,0. Bottom-right=100,100. Center=50,50. Use 3 decimals.
|
|
241
|
+
REGIONS: Left sidebar 5-20%, center 30-70%, right sidebar 75-95%
|
|
242
|
+
SANITY: Left sidebar at x=85%? WRONG (that's far right!)
|
|
348
243
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
- Top-right corner: xPercent=100, yPercent=0
|
|
352
|
-
- Bottom-left corner: xPercent=0, yPercent=100
|
|
353
|
-
- Bottom-right corner: xPercent=100, yPercent=100
|
|
354
|
-
- Center of screen: xPercent=50, yPercent=50
|
|
355
|
-
|
|
356
|
-
Use 3 decimal places for precision (e.g., 15.755, not 16).
|
|
357
|
-
|
|
358
|
-
ACTIONS:
|
|
359
|
-
|
|
360
|
-
**Physical clicks:**
|
|
361
|
-
- click: { action: "click", xPercent: 15.755, yPercent: 8.500 }
|
|
362
|
-
- doubleClick: { action: "doubleClick", xPercent: 15.755, yPercent: 8.500 }
|
|
363
|
-
- rightClick: { action: "rightClick", xPercent: 15.755, yPercent: 8.500 }
|
|
364
|
-
- hover: { action: "hover", xPercent: 15.755, yPercent: 8.500 }
|
|
365
|
-
|
|
366
|
-
**Input actions:**
|
|
367
|
-
- fill: Click then type
|
|
368
|
-
{ action: "fill", xPercent: 30.000, yPercent: 25.000, value: "alice@example.com" }
|
|
369
|
-
|
|
370
|
-
**Movement actions:**
|
|
371
|
-
- drag: From one position to another
|
|
372
|
-
{ action: "drag", xPercent: 10.000, yPercent: 50.000, toXPercent: 60.000, toYPercent: 50.000 }
|
|
373
|
-
- scroll: At position, scroll by amount
|
|
374
|
-
{ action: "scroll", xPercent: 50.000, yPercent: 50.000, scrollAmount: 500 }
|
|
375
|
-
|
|
376
|
-
CRITICAL RULES:
|
|
377
|
-
- Percentages are from viewport TOP-LEFT (not full page)
|
|
378
|
-
- Use element CENTER for coordinates, not edges
|
|
379
|
-
- Be precise with decimals - wrong coords click wrong element
|
|
380
|
-
- For fill: system will click at (x%,y%) then type value automatically
|
|
381
|
-
- For drag: toXPercent/toYPercent are REQUIRED
|
|
382
|
-
|
|
383
|
-
DO NOT try to generate selectors - that approach already failed. Use coordinates only.
|
|
384
|
-
This is a last-resort mechanism, but it WILL work if you provide accurate percentages.`;
|
|
244
|
+
OUTPUT: Return valid JSON with coordinateAction. Example: { "coordinateAction": { "action": "click", "xPercent": 15.755, "yPercent": 8.500 } }
|
|
245
|
+
Actions: click, fill (+ value), drag (+ toXPercent/toYPercent), scroll (+ scrollAmount). Aim for element CENTER.`;
|
|
385
246
|
}
|
|
386
247
|
/**
|
|
387
248
|
* Build user prompt with context
|
|
@@ -413,50 +274,36 @@ This is a last-resort mechanism, but it WILL work if you provide accurate percen
|
|
|
413
274
|
}
|
|
414
275
|
parts.push(``);
|
|
415
276
|
}
|
|
416
|
-
parts.push(`
|
|
417
|
-
parts.push(`- CRITICAL: First check if this step is STILL NEEDED (may already be done by prior step or now obsolete)`);
|
|
418
|
-
parts.push(` → If step goal already achieved/no longer needed: Return 0 commands + status "complete" (DELETE case)`);
|
|
419
|
-
parts.push(` → Example: "Dismiss modal" but modal already gone → 0 commands, status "complete"`);
|
|
420
|
-
parts.push(`- Use SoM markers to identify current elements`);
|
|
421
|
-
parts.push(`- Generate commands that work with CURRENT UI (not original script)`);
|
|
422
|
-
parts.push(`- CRITICAL: Once you fix this step, return status "complete" IMMEDIATELY (control goes back to script)`);
|
|
423
|
-
parts.push(` → Repair mode = single step fix, then hand back control`);
|
|
424
|
-
parts.push(` → Don't continue to next steps - script will auto-execute them`);
|
|
425
|
-
parts.push(`- DON'T redo completed steps - only fix the blocker\n`);
|
|
277
|
+
parts.push(`STRATEGY: Check if step still needed. Fix using current UI. Return "complete" when fixed.\n`);
|
|
426
278
|
}
|
|
427
|
-
//
|
|
428
|
-
parts.push('
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
parts.push('-
|
|
279
|
+
// Concise rules for LLM caching
|
|
280
|
+
parts.push('RULES: Do step goal only. No verify commands unless step asks. Check prerequisites before advancing.');
|
|
281
|
+
// TEMPORARY: Always full-page mode during debugging
|
|
282
|
+
// TODO: Re-enable adaptive heuristic once verified working
|
|
283
|
+
parts.push('- Screenshot shows FULL PAGE (all content visible, including offscreen elements)');
|
|
284
|
+
parts.push('- All interactive elements are marked with SoM IDs, even those below the fold');
|
|
285
|
+
// Kept for future reference when re-enabling adaptive mode:
|
|
286
|
+
// const heightOk = context.pageHeight && context.viewportHeight && context.pageHeight < (context.viewportHeight * 2);
|
|
287
|
+
// const widthOk = context.pageWidth && context.viewportWidth && context.pageWidth < (context.viewportWidth * 2);
|
|
288
|
+
// const isCompactPage = heightOk && widthOk;
|
|
432
289
|
parts.push('- Screenshot tool: Use ONCE for visual context, then ACT (max 3 per step, system enforced)');
|
|
433
290
|
parts.push('- Max 5 iterations per step, then forced STUCK\n');
|
|
434
291
|
// Dynamic content follows (changes per iteration)
|
|
435
292
|
parts.push('=== CURRENT CONTEXT ===\n');
|
|
436
293
|
// Display note from previous iteration (high priority tactical info)
|
|
437
|
-
if (context.
|
|
438
|
-
const note = context.
|
|
294
|
+
if (context.journeyMemory.latestNote) {
|
|
295
|
+
const note = context.journeyMemory.latestNote;
|
|
439
296
|
parts.push(`📝 YOUR NOTE FROM PREVIOUS ITERATION:`);
|
|
440
|
-
parts.push(
|
|
441
|
-
parts.push(` ^^ READ THIS - your previous self left important tactical guidance ^^`);
|
|
442
|
-
parts.push(``);
|
|
443
|
-
parts.push(` ACTION REQUIRED:`);
|
|
444
|
-
parts.push(` 1. Did your previous action work? Check the screenshot!`);
|
|
445
|
-
parts.push(` 2. If it WORKED: Execute next step from your plan`);
|
|
446
|
-
parts.push(` 3. If it FAILED: Use your backup plan (try alternative IDs/methods)`);
|
|
447
|
-
parts.push(` 4. Write NEW noteToFutureSelf with:`);
|
|
448
|
-
parts.push(` - What worked/didn't work (learn from attempts)`);
|
|
449
|
-
parts.push(` - Updated strategy with new backup plan`);
|
|
450
|
-
parts.push(` - Next alternatives to try if this fails`);
|
|
451
|
-
parts.push(` - Build on previous note's reasoning`);
|
|
297
|
+
parts.push(`${note.content}`);
|
|
452
298
|
parts.push(``);
|
|
453
|
-
parts.push(
|
|
299
|
+
parts.push(`⚠️ Follow your own instructions above. Compare current screenshot to expected state.`);
|
|
454
300
|
parts.push('');
|
|
455
301
|
}
|
|
456
302
|
// Check for screenshot loops (analysis paralysis) - PER STEP tracking
|
|
457
|
-
const
|
|
303
|
+
const recentSteps = context.journeyMemory.history.slice(-6);
|
|
304
|
+
const screenshotsThisStep = recentSteps.filter(s => s.stepNumber === context.stepNumber &&
|
|
458
305
|
(s.code.includes('take_screenshot') || s.action.toLowerCase().includes('screenshot')));
|
|
459
|
-
const recentScreenshots =
|
|
306
|
+
const recentScreenshots = recentSteps.slice(-3).filter(s => s.code.includes('take_screenshot') || s.action.toLowerCase().includes('screenshot'));
|
|
460
307
|
if (screenshotsThisStep.length >= 3) {
|
|
461
308
|
parts.push(`[CRITICAL] SCREENSHOT LOOP DETECTED - ${screenshotsThisStep.length} SCREENSHOTS THIS STEP`);
|
|
462
309
|
parts.push(`ANALYSIS PARALYSIS! You keep gathering info but NEVER ACTING!`);
|
|
@@ -474,7 +321,8 @@ This is a last-resort mechanism, but it WILL work if you provide accurate percen
|
|
|
474
321
|
parts.push(`[WARNING] SYSTEM WARNING: ${consecutiveFailures} failures!`);
|
|
475
322
|
// Only suggest screenshot if we haven't already taken multiple THIS STEP
|
|
476
323
|
if (screenshotsThisStep.length === 0) {
|
|
477
|
-
parts.push(`Take screenshot
|
|
324
|
+
parts.push(`Take full-page screenshot to see page state: { "name": "take_screenshot", "params": {"isFullPage": true} }`);
|
|
325
|
+
parts.push(`Then ACT with selector from the screenshot analysis.`);
|
|
478
326
|
}
|
|
479
327
|
else {
|
|
480
328
|
parts.push(`You already have visual context. Try different selector NOW.`);
|
|
@@ -503,11 +351,20 @@ This is a last-resort mechanism, but it WILL work if you provide accurate percen
|
|
|
503
351
|
// REPAIR MODE detection and instructions
|
|
504
352
|
const isRepairMode = context.priorSteps !== undefined;
|
|
505
353
|
if (isRepairMode) {
|
|
506
|
-
parts.push(`⚠️
|
|
354
|
+
parts.push(`⚠️ ⚠️ REPAIR MODE ⚠️ ⚠️`);
|
|
507
355
|
parts.push(`You are fixing a FAILED command from an existing script.`);
|
|
508
356
|
parts.push(`CRITICAL: The script executed command-by-command and stopped at a failure.`);
|
|
509
357
|
parts.push(`Your job: Fix ONLY the failing command. System will auto-execute remaining commands after.`);
|
|
510
|
-
parts.push(`⚠️
|
|
358
|
+
parts.push(`⚠️ ⚠️\n`);
|
|
359
|
+
// Show execution position summary
|
|
360
|
+
const successCount = context.successfulCommandsInCurrentStep?.length || 0;
|
|
361
|
+
const remainCount = context.remainingCommandsInCurrentStep?.length || 0;
|
|
362
|
+
const totalInStep = successCount + 1 + remainCount; // successful + failing + remaining
|
|
363
|
+
parts.push(`📍 EXECUTION POSITION:`);
|
|
364
|
+
parts.push(` Step ${context.stepNumber}/${context.totalSteps}: "${context.currentStepGoal}"`);
|
|
365
|
+
parts.push(` Command ${successCount + 1}/${totalInStep} in this step ← YOU ARE HERE (fixing this command)`);
|
|
366
|
+
parts.push(` ${successCount} commands succeeded before this`);
|
|
367
|
+
parts.push(` ${remainCount} commands will execute after your fix\n`);
|
|
511
368
|
if (context.successfulCommandsInCurrentStep && context.successfulCommandsInCurrentStep.length > 0) {
|
|
512
369
|
parts.push(`✅ SUCCESSFUL COMMANDS IN THIS STEP (already executed):`);
|
|
513
370
|
context.successfulCommandsInCurrentStep.forEach((cmd, idx) => {
|
|
@@ -547,7 +404,10 @@ This is a last-resort mechanism, but it WILL work if you provide accurate percen
|
|
|
547
404
|
parts.push(`🎯 CURRENT STEP GOAL (${context.stepNumber}/${context.totalSteps}):`);
|
|
548
405
|
parts.push(`${context.currentStepGoal}`);
|
|
549
406
|
parts.push(``);
|
|
550
|
-
parts.push(`[WARNING]
|
|
407
|
+
parts.push(`[WARNING] BEFORE STARTING: Do prerequisites from prior steps still exist?`);
|
|
408
|
+
parts.push(` Example: Step 4 "Click Core HR" needs Step 3's "menu expanded" state`);
|
|
409
|
+
parts.push(` → Check screenshot: Is menu still expanded? If NO, re-expand before Step 4!`);
|
|
410
|
+
parts.push(`[WARNING] AFTER ACTING: Is THIS step's goal achieved? If YES, mark status="complete" NOW.`);
|
|
551
411
|
parts.push(`[WARNING] CRITICAL: Only interact with elements you SEE in the screenshot - no guessing/hallucinating!`);
|
|
552
412
|
parts.push(`OVERALL SCENARIO: ${context.overallGoal}\n`);
|
|
553
413
|
if (!isRepairMode) {
|
|
@@ -560,50 +420,77 @@ This is a last-resort mechanism, but it WILL work if you provide accurate percen
|
|
|
560
420
|
}
|
|
561
421
|
// SoM screenshot (if available)
|
|
562
422
|
if (context.somScreenshot) {
|
|
563
|
-
parts.push(`\
|
|
564
|
-
parts.push(`Screenshot shows VIEWPORT ONLY (current visible area, not full page).`);
|
|
565
|
-
parts.push(`Color-coded bounding boxes mark interactive elements in the viewport.`);
|
|
566
|
-
parts.push(`Each element has a unique color and an ID label (1, 2, 3, etc.) at TOP-RIGHT corner, OUTSIDE the box.`);
|
|
567
|
-
parts.push(`Labels are typically positioned OUTSIDE and ABOVE the bounding box.`);
|
|
568
|
-
parts.push(`TO FIND THE CORRECT ELEMENT: match the label color with the bounding box color.`);
|
|
569
|
-
parts.push(`If target element not visible: SCROLL down/up OR use take_screenshot(isFullPage=true).`);
|
|
570
|
-
parts.push(`Reference element IDs in your commands using elementRef field (e.g., "1", "2", "42").`);
|
|
571
|
-
parts.push(`The screenshot is attached as an image - examine it to identify elements visually.`);
|
|
572
|
-
parts.push(``);
|
|
573
|
-
// SoM element map for disambiguation
|
|
423
|
+
parts.push(`\nSET-OF-MARKS: Full page with color-coded boxes + IDs. Match label color to box. Use IDs in elementRef.`);
|
|
574
424
|
if (context.somElementMap) {
|
|
575
|
-
parts.push(
|
|
576
|
-
parts.push(`If unsure which ID matches your target (e.g., is it 11 or 12?), use this map:`);
|
|
425
|
+
parts.push(`\nELEMENT MAP (for disambiguation):`);
|
|
577
426
|
parts.push(context.somElementMap);
|
|
578
|
-
parts.push(`Example: If you need a "Submit" button and see IDs 5 and 6 are both buttons, check the map to see which one says "Submit".`);
|
|
579
427
|
parts.push(``);
|
|
580
428
|
}
|
|
581
429
|
}
|
|
582
430
|
// Current page state (most variable content - at the end)
|
|
583
431
|
parts.push(`\nCURRENT PAGE:`);
|
|
584
432
|
parts.push(`URL: ${context.currentURL}`);
|
|
585
|
-
parts.push(`Title: ${context.
|
|
586
|
-
//
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
parts.push(
|
|
433
|
+
parts.push(`Title: ${context.currentPageTitle}`);
|
|
434
|
+
// Page dimensions for scroll decisions
|
|
435
|
+
if (context.viewportWidth && context.viewportHeight && context.pageHeight) {
|
|
436
|
+
const heightOk = context.pageHeight < (context.viewportHeight * 2);
|
|
437
|
+
const widthOk = context.pageWidth && context.pageWidth < (context.viewportWidth * 2);
|
|
438
|
+
const isCompactPage = heightOk && widthOk;
|
|
439
|
+
parts.push(`\nPAGE DIMENSIONS & SCROLL POSITION:`);
|
|
440
|
+
parts.push(`Viewport: ${context.viewportWidth}x${context.viewportHeight}px`);
|
|
441
|
+
parts.push(`Full Page: ${context.pageWidth}x${context.pageHeight}px`);
|
|
442
|
+
parts.push(`Screenshot Mode: ${isCompactPage ? 'FULL PAGE (compact page, all visible)' : 'VIEWPORT ONLY (large page, full-page markers would be too small)'}`);
|
|
443
|
+
if (context.scrollY !== undefined && context.scrollY > 0) {
|
|
444
|
+
parts.push(`Current Scroll: ${context.scrollY}px from top (you've already scrolled down)`);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
parts.push(`Current Scroll: At top of page (scrollY = 0)`);
|
|
448
|
+
}
|
|
449
|
+
const canScrollDown = context.pageHeight > context.viewportHeight;
|
|
450
|
+
const canScrollRight = context.pageWidth && context.pageWidth > context.viewportWidth;
|
|
451
|
+
if (!isCompactPage && (canScrollDown || canScrollRight)) {
|
|
452
|
+
const remainingBelow = Math.max(0, context.pageHeight - context.viewportHeight - (context.scrollY || 0));
|
|
453
|
+
const remainingRight = context.pageWidth ? Math.max(0, context.pageWidth - context.viewportWidth - (context.scrollX || 0)) : 0;
|
|
454
|
+
const hiddenContent = [];
|
|
455
|
+
if (remainingBelow > 0)
|
|
456
|
+
hiddenContent.push(`${remainingBelow}px below`);
|
|
457
|
+
if (remainingRight > 0)
|
|
458
|
+
hiddenContent.push(`${remainingRight}px to right`);
|
|
459
|
+
if (hiddenContent.length > 0) {
|
|
460
|
+
parts.push(`Hidden content: ${hiddenContent.join(', ')}`);
|
|
461
|
+
parts.push(`💡 If element not found → Call: take_screenshot with {"isFullPage": true, "purpose": "Find X"}`);
|
|
462
|
+
parts.push(` This shows entire page (markers small but LLM can still locate elements)`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
else if (isCompactPage) {
|
|
466
|
+
parts.push(`All content visible in screenshot (no need for additional tools)`);
|
|
467
|
+
}
|
|
592
468
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
469
|
+
// In SoM mode, element details are in somElementMap (visual screenshot)
|
|
470
|
+
parts.push(`\nNote: Element details available in visual screenshot with SoM markers.`);
|
|
471
|
+
parts.push('');
|
|
472
|
+
// Show current URL with change detection
|
|
473
|
+
const currentUrl = truncateUrl(context.currentURL || '');
|
|
474
|
+
const lastAction = context.journeyMemory.history[context.journeyMemory.history.length - 1];
|
|
475
|
+
if (lastAction && lastAction.previousUrl && lastAction.url !== lastAction.previousUrl) {
|
|
476
|
+
const prevUrl = truncateUrl(lastAction.previousUrl);
|
|
477
|
+
const newUrl = truncateUrl(lastAction.url);
|
|
478
|
+
parts.push(`🔄 URL CHANGED: ${prevUrl} → ${newUrl}`);
|
|
479
|
+
parts.push(` ⚠️ Navigation occurred! Previous action likely succeeded and triggered page transition.\n`);
|
|
596
480
|
}
|
|
597
|
-
|
|
598
|
-
parts.push(
|
|
481
|
+
else {
|
|
482
|
+
parts.push(`📍 Current URL: ${currentUrl}\n`);
|
|
599
483
|
}
|
|
600
|
-
parts.push('');
|
|
601
484
|
// Recent steps (most variable content - at the end)
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
485
|
+
const recentStepsDisplay = context.journeyMemory.history.slice(-6);
|
|
486
|
+
if (recentStepsDisplay.length > 0) {
|
|
487
|
+
parts.push(`RECENT STEPS (last ${recentStepsDisplay.length}):`);
|
|
488
|
+
for (const step of recentStepsDisplay) {
|
|
605
489
|
const status = step.result === 'success' ? '[OK]' : '[FAIL]';
|
|
606
|
-
|
|
490
|
+
const urlChanged = step.previousUrl && step.url !== step.previousUrl
|
|
491
|
+
? ` [URL: ${step.previousUrl} → ${step.url}]`
|
|
492
|
+
: '';
|
|
493
|
+
parts.push(` ${status} ${step.stepNumber}.${step.iteration || ''} ${step.action}${urlChanged}`);
|
|
607
494
|
parts.push(` Code: ${step.code}`);
|
|
608
495
|
if (step.result === 'failure' && step.error) {
|
|
609
496
|
parts.push(` ERROR: ${step.error}`);
|
|
@@ -615,7 +502,7 @@ This is a last-resort mechanism, but it WILL work if you provide accurate percen
|
|
|
615
502
|
}
|
|
616
503
|
parts.push('');
|
|
617
504
|
// Detect repeated failures
|
|
618
|
-
const recentFailures =
|
|
505
|
+
const recentFailures = recentStepsDisplay.filter(s => s.result === 'failure');
|
|
619
506
|
if (recentFailures.length >= 2) {
|
|
620
507
|
const sameSelector = recentFailures.slice(-2).every((s, i, arr) => i === 0 || s.code === arr[i - 1].code);
|
|
621
508
|
if (sameSelector) {
|
|
@@ -625,18 +512,49 @@ This is a last-resort mechanism, but it WILL work if you provide accurate percen
|
|
|
625
512
|
}
|
|
626
513
|
}
|
|
627
514
|
}
|
|
628
|
-
//
|
|
629
|
-
if (context.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
515
|
+
// Site learnings (persistent knowledge)
|
|
516
|
+
if (context.siteLearnings) {
|
|
517
|
+
const { screens, uxPatterns } = context.siteLearnings;
|
|
518
|
+
// Display UX patterns with IDs
|
|
519
|
+
const uxPatternEntries = Object.entries(uxPatterns);
|
|
520
|
+
if (uxPatternEntries.length > 0) {
|
|
521
|
+
parts.push(`\n🎯 SITE-WIDE UX PATTERNS (reference [ID] for updates/deletes):`);
|
|
522
|
+
uxPatternEntries.forEach(([id, text]) => parts.push(` [${id}] ${text}`));
|
|
523
|
+
parts.push('');
|
|
524
|
+
}
|
|
525
|
+
// Display screen/state vocabulary first (for consistent naming)
|
|
526
|
+
if (context.siteLearnings?.screenStateVocabulary && Object.keys(context.siteLearnings.screenStateVocabulary).length > 0) {
|
|
527
|
+
parts.push(`\n📋 SCREEN STATE VOCABULARY (use these names for consistency):`);
|
|
528
|
+
Object.entries(context.siteLearnings.screenStateVocabulary).forEach(([screenName, stateNames]) => {
|
|
529
|
+
const statesDisplay = stateNames.length > 0
|
|
530
|
+
? ` → States: ${stateNames.map(s => s || '""').join(', ')}`
|
|
531
|
+
: '';
|
|
532
|
+
parts.push(` • ${screenName}${statesDisplay}`);
|
|
533
|
+
});
|
|
534
|
+
parts.push('');
|
|
535
|
+
}
|
|
536
|
+
// Display screen state knowledge with IDs
|
|
537
|
+
if (screens && Object.keys(screens).length > 0) {
|
|
538
|
+
parts.push(`\n📚 SCREEN STATE KNOWLEDGE (reference [ID] for updates/deletes):`);
|
|
539
|
+
Object.entries(screens).forEach(([screenName, screenLearnings]) => {
|
|
540
|
+
Object.entries(screenLearnings.states).forEach(([state, learning]) => {
|
|
541
|
+
const stateLabel = state ? `[${state}]` : '';
|
|
542
|
+
parts.push(`\n ${screenName}${stateLabel}:`);
|
|
543
|
+
const obsEntries = Object.entries(learning.observations);
|
|
544
|
+
if (obsEntries.length > 0) {
|
|
545
|
+
obsEntries.forEach(([id, text]) => {
|
|
546
|
+
parts.push(` [${id}] ${text}`);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
parts.push('');
|
|
633
552
|
}
|
|
634
|
-
parts.push('');
|
|
635
553
|
}
|
|
636
554
|
// Extracted data (from previous extract_data tool calls)
|
|
637
|
-
if (context.extractedData && Object.keys(context.extractedData).length > 0) {
|
|
555
|
+
if (context.journeyMemory.extractedData && Object.keys(context.journeyMemory.extractedData).length > 0) {
|
|
638
556
|
parts.push(`\nEXTRACTED DATA (available for use in commands):`);
|
|
639
|
-
parts.push(JSON.stringify(context.extractedData, null, 2));
|
|
557
|
+
parts.push(JSON.stringify(context.journeyMemory.extractedData, null, 2));
|
|
640
558
|
parts.push('');
|
|
641
559
|
}
|
|
642
560
|
return parts.join('\n');
|
|
@@ -653,19 +571,20 @@ DISCRETE EXPERIENCE LOOP:
|
|
|
653
571
|
You operate in iterations: receive state → decide → sleep → wake with new state.
|
|
654
572
|
System waits for page stability after each batch.
|
|
655
573
|
|
|
656
|
-
CRITICAL: MEMORY
|
|
657
|
-
|
|
658
|
-
- Current screenshot
|
|
659
|
-
-
|
|
660
|
-
-
|
|
574
|
+
CRITICAL: NO SCREENSHOT MEMORY (STATELESS!)
|
|
575
|
+
Each iteration you receive:
|
|
576
|
+
- Current screenshot (NOT previous screenshots!)
|
|
577
|
+
- Past actions (text descriptions, not screenshots)
|
|
578
|
+
- Ongoing memory (experiences, patterns)
|
|
661
579
|
- Your noteToFutureSelf from last iteration
|
|
580
|
+
- Current journey goal
|
|
662
581
|
|
|
663
|
-
The noteToFutureSelf is your
|
|
664
|
-
•
|
|
665
|
-
•
|
|
666
|
-
•
|
|
667
|
-
•
|
|
668
|
-
|
|
582
|
+
The noteToFutureSelf is your way to document expectations for verification. MUST include EXPLICIT EXPECTED STATE:
|
|
583
|
+
• ✅ GOOD: "Clicked sidebar menu button (was collapsed). EXPECT: expanded sidebar with 'Dashboard' and 'Reports' visible"
|
|
584
|
+
• ✅ GOOD: "Navigated to /settings. EXPECT: URL changed, 'Save Settings' button visible"
|
|
585
|
+
• ❌ BAD: "Clicked menu" (future you can't verify if it worked!)
|
|
586
|
+
• ❌ BAD: "Clicked ID 8" (ID meaningless without screenshot!)
|
|
587
|
+
Also include: strategy, observations, patterns discovered, backup plans if this fails
|
|
669
588
|
|
|
670
589
|
COMMON UX PATTERNS (critical for navigation):
|
|
671
590
|
• Disabled buttons → Fill required fields first to enable them
|
|
@@ -680,31 +599,13 @@ COMMON UX PATTERNS (critical for navigation):
|
|
|
680
599
|
• Lazy loading → Scroll down to load more content
|
|
681
600
|
• Accordions/expandable → Click header to toggle visibility
|
|
682
601
|
|
|
683
|
-
|
|
602
|
+
${RESPONSE_SCHEMA}
|
|
684
603
|
|
|
685
|
-
interface
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
// COMMANDS: Array of plain Playwright command strings
|
|
691
|
-
commands?: string[]; // Example: ["await page.fill('input[name=\"email\"]', 'test@example.com')", ...]
|
|
692
|
-
commandReasoning?: string;
|
|
693
|
-
toolCalls?: Array<{ // Tools to call (extract_data for menus, etc.)
|
|
694
|
-
name: string;
|
|
695
|
-
params: Record<string, any>;
|
|
696
|
-
}>;
|
|
697
|
-
toolReasoning?: string;
|
|
698
|
-
needsToolResults?: boolean;
|
|
699
|
-
noteToFutureSelf?: string;
|
|
700
|
-
coordinateAction?: { ... };
|
|
701
|
-
experiences?: string[]; // Use for BOTH app patterns AND exploration progress
|
|
702
|
-
blockerDetected?: { ... };
|
|
703
|
-
debugInfo?: { // Meta-learning: suggest prompt improvements (only when very confident)
|
|
704
|
-
suggestedPromptUpdates?: string;
|
|
705
|
-
reasoning?: string;
|
|
706
|
-
};
|
|
707
|
-
}
|
|
604
|
+
YOUR RESPONSE FORMAT - Output JSON matching AgentDecision interface above.
|
|
605
|
+
|
|
606
|
+
For exploration mode, also include:
|
|
607
|
+
- stepSummary: Concise 1-sentence summary of what was accomplished this iteration
|
|
608
|
+
- commands: Array of plain Playwright command strings (exploration uses string commands, not SoM)
|
|
708
609
|
|
|
709
610
|
EXPLORATION MODE GUIDELINES:
|
|
710
611
|
|
|
@@ -717,7 +618,7 @@ EXPLORATION MODE GUIDELINES:
|
|
|
717
618
|
|
|
718
619
|
3. **VISIBLE ELEMENTS ONLY**: Screenshot shows viewport only. Only interact with elements you SEE. If not visible, scroll or take_screenshot(isFullPage=true).
|
|
719
620
|
|
|
720
|
-
4. **SYSTEMATIC EXPLORATION**: Use extract_data to discover, store in extractedData, track in
|
|
621
|
+
4. **SYSTEMATIC EXPLORATION**: Use extract_data to discover, store in extractedData, track in siteLearningsUpdate, check history to avoid repeating, prioritize unexplored areas.
|
|
721
622
|
|
|
722
623
|
5. **CREATIVE TESTING**: Test functionality thoroughly - try edge cases, verify features work, look for bugs.
|
|
723
624
|
|
|
@@ -732,7 +633,25 @@ EXPLORATION MODE GUIDELINES:
|
|
|
732
633
|
|
|
733
634
|
11. **STEP SUMMARY**: When you complete actions, provide a concise 1-sentence summary of what was accomplished (e.g., "Logged in successfully", "Navigated to dashboard", "Created new widget"). This is used for step tracking, not future planning.
|
|
734
635
|
|
|
735
|
-
12. **MEMORY**:
|
|
636
|
+
12. **MEMORY (STATELESS!)**: You see only current screenshot. MUST write expected state in noteToFutureSelf:
|
|
637
|
+
- ✅ "Clicked settings button in navbar. EXPECT: settings page with 'Profile' section visible"
|
|
638
|
+
- ❌ "Clicked settings" (can't verify!)
|
|
639
|
+
- ❌ "Clicked ID 9" (ID meaningless without screenshot!)
|
|
640
|
+
- siteLearningsUpdate=persistent knowledge, extractedData=journey discoveries
|
|
641
|
+
|
|
642
|
+
SITE LEARNINGS: Build mental model (persistent across journeys)
|
|
643
|
+
- screenState: {screen, state} to identify current context (NEVER: "about:blank", "loading" states)
|
|
644
|
+
- siteLearningsUpdate: Add/update/delete observations per screen-state
|
|
645
|
+
CRITICAL: NEVER include SoM IDs ("element 9", "ID 5") - they regenerate every page load!
|
|
646
|
+
✅ "Workspace selector opens on caret icon click"
|
|
647
|
+
❌ "Element 9 opens dropdown with entries 6,7,8"
|
|
648
|
+
|
|
649
|
+
WHEN TO STORE:
|
|
650
|
+
✅ After discovering navigation (uxPatterns)
|
|
651
|
+
✅ After learning UI behavior (uxPatterns)
|
|
652
|
+
✅ When understanding screen layout (observations)
|
|
653
|
+
✅ When selector fails (observations)
|
|
654
|
+
❌ Don't store obvious/temporary things
|
|
736
655
|
|
|
737
656
|
CRITICAL: You're fully autonomous for THIS journey - no step-by-step instructions provided.
|
|
738
657
|
YOU decide the exploration path to meet the journey goal based on: journey prompt, current state, and memory.`;
|
|
@@ -764,21 +683,20 @@ YOU decide the exploration path to meet the journey goal based on: journey promp
|
|
|
764
683
|
parts.push(`PROGRESS: Step ${stepNumber}/${maxSteps} (you can complete earlier if journey goal met)\n`);
|
|
765
684
|
}
|
|
766
685
|
// Show discovered and tracked data from extractedData
|
|
767
|
-
if (context.extractedData && Object.keys(context.extractedData).length > 0) {
|
|
686
|
+
if (context.journeyMemory.extractedData && Object.keys(context.journeyMemory.extractedData).length > 0) {
|
|
768
687
|
parts.push(`\nDISCOVERED DATA (this journey):`);
|
|
769
|
-
for (const [key, value] of Object.entries(context.extractedData)) {
|
|
688
|
+
for (const [key, value] of Object.entries(context.journeyMemory.extractedData)) {
|
|
770
689
|
parts.push(` ${key}: ${value}`);
|
|
771
690
|
}
|
|
772
691
|
}
|
|
773
692
|
// SoM screenshot (if available)
|
|
774
693
|
if (context.somScreenshot) {
|
|
775
694
|
parts.push(`\n SET-OF-MARKS SCREENSHOT (with element IDs):`);
|
|
776
|
-
parts.push(`Screenshot shows
|
|
777
|
-
parts.push(`Color-coded bounding boxes mark interactive elements
|
|
695
|
+
parts.push(`Screenshot shows FULL PAGE (all content, including below-fold elements).`);
|
|
696
|
+
parts.push(`Color-coded bounding boxes mark ALL interactive elements across entire page.`);
|
|
778
697
|
parts.push(`Each element has a unique color and an ID label (1, 2, 3, etc.) at TOP-RIGHT corner, OUTSIDE the box.`);
|
|
779
698
|
parts.push(`Labels are typically positioned OUTSIDE and ABOVE the bounding box.`);
|
|
780
699
|
parts.push(`TO FIND THE CORRECT ELEMENT: match the label color with the bounding box color.`);
|
|
781
|
-
parts.push(`If target element not visible: SCROLL down/up OR use take_screenshot(isFullPage=true).`);
|
|
782
700
|
parts.push(`Reference element IDs in your commands using elementRef field (e.g., "1", "2", "42").`);
|
|
783
701
|
parts.push(`The screenshot is attached as an image - examine it to identify elements visually.`);
|
|
784
702
|
parts.push(``);
|
|
@@ -799,41 +717,72 @@ YOU decide the exploration path to meet the journey goal based on: journey promp
|
|
|
799
717
|
}
|
|
800
718
|
parts.push(`\nCURRENT PAGE:`);
|
|
801
719
|
parts.push(`URL: ${context.currentURL}`);
|
|
802
|
-
parts.push(`Title: ${context.
|
|
803
|
-
//
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
720
|
+
parts.push(`Title: ${context.currentPageTitle}`);
|
|
721
|
+
// In SoM mode, element details are in somElementMap
|
|
722
|
+
parts.push(`\nNote: Element details available in visual screenshot with SoM markers.`);
|
|
723
|
+
// Recent actions
|
|
724
|
+
// Show current URL with change detection
|
|
725
|
+
const currentUrl = truncateUrl(context.currentURL || '');
|
|
726
|
+
const lastAction = context.journeyMemory.history[context.journeyMemory.history.length - 1];
|
|
727
|
+
if (lastAction && lastAction.previousUrl && lastAction.url !== lastAction.previousUrl) {
|
|
728
|
+
const prevUrl = truncateUrl(lastAction.previousUrl);
|
|
729
|
+
const newUrl = truncateUrl(lastAction.url);
|
|
730
|
+
parts.push(`\n🔄 URL CHANGED: ${prevUrl} → ${newUrl}`);
|
|
731
|
+
parts.push(` ⚠️ Navigation occurred! Previous action likely triggered page transition.\n`);
|
|
809
732
|
}
|
|
810
733
|
else {
|
|
811
|
-
|
|
812
|
-
parts.push(`\nNote: Element details available in visual screenshot with SoM markers.`);
|
|
734
|
+
parts.push(`\n📍 Current URL: ${currentUrl}\n`);
|
|
813
735
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (context.recentSteps.length > 0) {
|
|
819
|
-
parts.push(`\nRECENT ACTIONS (last ${context.recentSteps.length}):`);
|
|
820
|
-
for (const step of context.recentSteps) {
|
|
736
|
+
const recentActions = context.journeyMemory.history.slice(-6);
|
|
737
|
+
if (recentActions.length > 0) {
|
|
738
|
+
parts.push(`RECENT ACTIONS (last ${recentActions.length}):`);
|
|
739
|
+
for (const step of recentActions) {
|
|
821
740
|
const status = step.result === 'success' ? '[OK]' : '[FAIL]';
|
|
822
|
-
|
|
741
|
+
const urlChanged = step.previousUrl && step.url !== step.previousUrl
|
|
742
|
+
? ` [URL: ${truncateUrl(step.previousUrl)} → ${truncateUrl(step.url)}]`
|
|
743
|
+
: '';
|
|
744
|
+
parts.push(` ${status} ${step.action}${urlChanged}`);
|
|
823
745
|
parts.push(` ${step.observation}`);
|
|
824
746
|
}
|
|
825
747
|
}
|
|
826
|
-
//
|
|
827
|
-
if (context.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
748
|
+
// Site learnings
|
|
749
|
+
if (context.siteLearnings) {
|
|
750
|
+
const { screens, uxPatterns } = context.siteLearnings;
|
|
751
|
+
const uxPatternEntries = Object.entries(uxPatterns);
|
|
752
|
+
if (uxPatternEntries.length > 0) {
|
|
753
|
+
parts.push(`\n🎯 SITE-WIDE UX PATTERNS (reference [ID] for updates/deletes):`);
|
|
754
|
+
uxPatternEntries.forEach(([id, text]) => parts.push(` [${id}] ${text}`));
|
|
755
|
+
}
|
|
756
|
+
// Display screen/state vocabulary first (for consistent naming)
|
|
757
|
+
if (context.siteLearnings?.screenStateVocabulary && Object.keys(context.siteLearnings.screenStateVocabulary).length > 0) {
|
|
758
|
+
parts.push(`\n📋 SCREEN STATE VOCABULARY (use these names for consistency):`);
|
|
759
|
+
Object.entries(context.siteLearnings.screenStateVocabulary).forEach(([screenName, stateNames]) => {
|
|
760
|
+
const statesDisplay = stateNames.length > 0
|
|
761
|
+
? ` → States: ${stateNames.map(s => s || '""').join(', ')}`
|
|
762
|
+
: '';
|
|
763
|
+
parts.push(` • ${screenName}${statesDisplay}`);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
if (screens && Object.keys(screens).length > 0) {
|
|
767
|
+
parts.push(`\n📚 SCREEN STATE KNOWLEDGE (reference [ID] for updates/deletes):`);
|
|
768
|
+
Object.entries(screens).forEach(([screenName, screenLearnings]) => {
|
|
769
|
+
Object.entries(screenLearnings.states).forEach(([state, learning]) => {
|
|
770
|
+
const stateLabel = state ? `[${state}]` : '';
|
|
771
|
+
parts.push(`\n ${screenName}${stateLabel}:`);
|
|
772
|
+
const obsEntries = Object.entries(learning.observations);
|
|
773
|
+
if (obsEntries.length > 0) {
|
|
774
|
+
obsEntries.forEach(([id, text]) => {
|
|
775
|
+
parts.push(` [${id}] ${text}`);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
});
|
|
831
780
|
}
|
|
832
781
|
}
|
|
833
782
|
// Note from previous iteration
|
|
834
|
-
if (context.
|
|
835
|
-
parts.push(`\nYOUR NOTE FROM LAST ITERATION: ${context.
|
|
836
|
-
parts.push(`
|
|
783
|
+
if (context.journeyMemory.latestNote) {
|
|
784
|
+
parts.push(`\nYOUR NOTE FROM LAST ITERATION: ${context.journeyMemory.latestNote.content}`);
|
|
785
|
+
parts.push(` ^^ Follow your own instructions from previous iteration ^^`);
|
|
837
786
|
}
|
|
838
787
|
parts.push(`\nDECIDE NEXT ACTION: What to explore/test next? Check history to avoid repeating. Is goal achieved? Mark complete.`);
|
|
839
788
|
return parts.join('\n');
|