testronaut 1.0.8 ā 1.0.9
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/core/agent.js +0 -37
- package/core/turnLoop.js +20 -97
- package/package.json +9 -2
- package/runner/missionLaunch.js +1 -1
- package/runner/testronaut.js +0 -36
- package/tools/chromeBrowser.js +1 -6
package/core/agent.js
CHANGED
|
@@ -2,33 +2,6 @@ import 'dotenv/config';
|
|
|
2
2
|
import { ChromeBrowser } from '../tools/chromeBrowser.js';
|
|
3
3
|
import { turnLoop } from './turnLoop.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
// export async function runAgent(goal, maxTurns = 10) {
|
|
7
|
-
// console.log(`\nš Running Agent with goal: ${goal}`);
|
|
8
|
-
// const browser = new ChromeBrowser();
|
|
9
|
-
// await browser.start();
|
|
10
|
-
|
|
11
|
-
// const messages = [
|
|
12
|
-
// {
|
|
13
|
-
// role: 'system',
|
|
14
|
-
// content: `
|
|
15
|
-
// You are an autonomous web agent. Use function calls to complete the user's goal.
|
|
16
|
-
// If you are unsure of the selectors for inputs or buttons, call 'get_dom' to retrieve page HTML,
|
|
17
|
-
// analyze it, then make your best guess based on labels, names, types, and placeholder values.
|
|
18
|
-
// After completing the goal, respond with a final plain-text message starting with SUCCESS or FAILURE.
|
|
19
|
-
// `.trim(),
|
|
20
|
-
// },
|
|
21
|
-
// { role: 'user', content: goal },
|
|
22
|
-
// ];
|
|
23
|
-
|
|
24
|
-
// try {
|
|
25
|
-
// const result = await turnLoop(browser, messages, maxTurns);
|
|
26
|
-
// !result && console.log('š Agent ran out of turns.')
|
|
27
|
-
// } finally {
|
|
28
|
-
// await browser.close();
|
|
29
|
-
// }
|
|
30
|
-
// }
|
|
31
|
-
|
|
32
5
|
export async function runAgent(goals, missionName, maxTurns = 20, ) {
|
|
33
6
|
const browser = new ChromeBrowser();
|
|
34
7
|
await browser.start();
|
|
@@ -86,13 +59,3 @@ export async function runAgent(goals, missionName, maxTurns = 20, ) {
|
|
|
86
59
|
await browser.close();
|
|
87
60
|
}
|
|
88
61
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// (async () => {
|
|
92
|
-
// await runAgent(`
|
|
93
|
-
// Visit ${process.env.URL}.
|
|
94
|
-
// Log in using ${process.env.USERNAME} and password ${process.env.PASSWORD}.
|
|
95
|
-
// After clicking the login button, use 'check_text' to verify that "${process.env.AFTER_LOGIN_CHECK}" appears on the page.
|
|
96
|
-
// If found, report SUCCESS. Otherwise, report FAILURE.
|
|
97
|
-
// `);
|
|
98
|
-
// })();
|
package/core/turnLoop.js
CHANGED
|
@@ -18,6 +18,7 @@ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
|
18
18
|
|
|
19
19
|
let totalTokensUsed = 0;
|
|
20
20
|
let turnTimestamps = [];
|
|
21
|
+
let shouldBackoff;
|
|
21
22
|
|
|
22
23
|
const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool } = {}) => {
|
|
23
24
|
if (skipIfLastTool && skipIfLastTool.includes(messages.at(-1)?.name)) {
|
|
@@ -61,48 +62,22 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
61
62
|
|
|
62
63
|
for (let turn = currentTurn; turn < maxTurns; turn++) {
|
|
63
64
|
console.log(`\nš Turn ${turn + 1}/${maxTurns}`);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// const now = Date.now();
|
|
67
|
-
// turnTimestamps = turnTimestamps.filter(ts => now - ts < 60000);
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
turnTimestamps = pruned.turnTimestamps;
|
|
72
|
-
totalTokensUsed = pruned.totalTokensUsed;
|
|
68
|
+
// Recalculate the rolling window before checking cooldown
|
|
69
|
+
({ turnTimestamps, totalTokensUsed } = pruneOldTokenUsage(turnTimestamps));
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
turnTimestamps = cooldownResult.turnTimestamps;
|
|
78
|
-
if (cooldownResult.shouldBackoff) {
|
|
79
|
-
return await turnLoop(browser, messages, maxTurns, turn);
|
|
80
|
-
}
|
|
81
|
-
// if (await tokenUseCoolOff(totalTokensUsed, turnTimestamps)) {return await turnLoop(browser, messages, maxTurns, turn)}
|
|
71
|
+
// Cool off check
|
|
72
|
+
({ totalTokensUsed, turnTimestamps, shouldBackoff } = await tokenUseCoolOff(totalTokensUsed, turnTimestamps));
|
|
73
|
+
if(shouldBackoff) return await turnLoop(browser, messages, maxTurns, turn);
|
|
82
74
|
|
|
83
|
-
|
|
84
|
-
insertPlaceholders: true
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
console.error('ā Tool call structure invalid and no placeholders inserted.');
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
// console.log(JSON.stringify(messages, null, 2));
|
|
75
|
+
//Tool Validator
|
|
76
|
+
if (!validateAndInsertMissingToolResponses(messages, { insertPlaceholders: true })) {
|
|
77
|
+
console.error('ā Tool call structure invalid and no placeholders inserted.');
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
92
80
|
|
|
93
|
-
let response;
|
|
94
|
-
try {
|
|
95
|
-
// console.log(
|
|
96
|
-
// 'š Verifying message structure before OpenAI call:\n',
|
|
97
|
-
// messages.map(m => ({
|
|
98
|
-
// role: m.role,
|
|
99
|
-
// name: m.name,
|
|
100
|
-
// tool_call_id: m.tool_call_id,
|
|
101
|
-
// tool_calls: m.tool_calls?.map(t => t.id),
|
|
102
|
-
// content: m.content?.slice?.(0, 100), // truncate for readability
|
|
103
|
-
// }))
|
|
104
|
-
// );
|
|
105
|
-
|
|
106
81
|
const unrespondedCalls = messages.filter(
|
|
107
82
|
(msg, i) =>
|
|
108
83
|
msg.role === 'assistant' &&
|
|
@@ -140,8 +115,8 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
140
115
|
return await turnLoop(browser, messages, maxTurns, turn, retryCount + 1);
|
|
141
116
|
} else if (err.status === 400) {
|
|
142
117
|
console.error('ā Bad request:', err.message);
|
|
143
|
-
console.log("current tools", toolsSchema);
|
|
144
|
-
console.log("messages: ", messages)
|
|
118
|
+
// console.log("current tools", toolsSchema);
|
|
119
|
+
// console.log("messages: ", messages)
|
|
145
120
|
return false;
|
|
146
121
|
} else {
|
|
147
122
|
throw err;
|
|
@@ -149,40 +124,23 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
149
124
|
}
|
|
150
125
|
console.log('Response received from OpenAI');
|
|
151
126
|
const usage = response.usage;
|
|
152
|
-
// if (usage) {
|
|
153
|
-
// console.log(`š Token Usage This Turn ā Prompt: ${usage.prompt_tokens}, Completion: ${usage.completion_tokens}, Total: ${usage.total_tokens}`);
|
|
154
|
-
// totalTokensUsed += usage?.total_tokens || 0;
|
|
155
|
-
// turnTimestamps.push(now);
|
|
156
|
-
// console.log(`š Running Total Tokens Used: ${totalTokensUsed}`);
|
|
157
|
-
// }
|
|
158
127
|
if (usage) {
|
|
159
128
|
const tokensUsed = usage.total_tokens || 0;
|
|
160
129
|
console.log(`š Token Usage This Turn ā Total: ${tokensUsed}`);
|
|
161
130
|
recordTokenUsage(turnTimestamps, tokensUsed);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
totalTokensUsed = pruned.totalTokensUsed;
|
|
131
|
+
({ turnTimestamps, totalTokensUsed } = pruneOldTokenUsage(turnTimestamps));
|
|
132
|
+
|
|
165
133
|
console.log(`š Running Total Tokens Used (Rolling 60s): ${totalTokensUsed}`);
|
|
166
134
|
}
|
|
167
135
|
|
|
168
136
|
const msg = response.choices[0].message;
|
|
169
137
|
|
|
170
|
-
// if (!msg.tool_calls?.length) {
|
|
171
|
-
// console.error('ā Assistant message had empty or missing tool_calls array');
|
|
172
|
-
// return false;
|
|
173
|
-
// }
|
|
174
|
-
|
|
175
138
|
if (msg.tool_calls?.length) {
|
|
176
139
|
console.log('Processing tool calls...');
|
|
177
|
-
// messages.push(msg);
|
|
178
140
|
const toolResponses = [];
|
|
179
141
|
let lastToolName = null;
|
|
180
142
|
|
|
181
143
|
for (const call of msg.tool_calls) {
|
|
182
|
-
const toolCallId = call.id;
|
|
183
|
-
// const hasResponse = messages.some(m =>
|
|
184
|
-
// m.role === 'tool' && m.tool_call_id === toolCallId
|
|
185
|
-
// );
|
|
186
144
|
const fnName = call.function.name;
|
|
187
145
|
lastToolName = fnName;
|
|
188
146
|
const args = JSON.parse(call.function.arguments || '{}');
|
|
@@ -194,15 +152,6 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
194
152
|
// console.log("agentMemory: ", agentMemory);
|
|
195
153
|
result = await CHROME_TOOL_MAP[fnName](browser, args, agentMemory);
|
|
196
154
|
|
|
197
|
-
// Smart DOM-ready wait after major actions like click
|
|
198
|
-
// if (['click', 'click_text', 'fill'].includes(fnName)) {
|
|
199
|
-
// try {
|
|
200
|
-
// await browser.page.waitForLoadState?.('domcontentloaded', { timeout: 5000 });
|
|
201
|
-
// await browser.page.waitForTimeout?.(2000); // allow animations/navigation
|
|
202
|
-
// } catch (err) {
|
|
203
|
-
// console.warn(`ā ļø DOM readiness wait failed after ${fnName}: ${err.message}`);
|
|
204
|
-
// }
|
|
205
|
-
// }
|
|
206
155
|
if (typeof result !== 'string') {
|
|
207
156
|
result = JSON.stringify(result ?? '');
|
|
208
157
|
}
|
|
@@ -211,26 +160,9 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
211
160
|
result = errorMessage;
|
|
212
161
|
}
|
|
213
162
|
|
|
214
|
-
// š Diagnostic: log whether this tool call succeeded or failed
|
|
215
163
|
console.log(`[tool ] ā ${fnName} result:`, errorMessage ? 'ā Failed' : 'ā
Success');
|
|
216
|
-
|
|
217
164
|
const truncated = result.length > 100 ? result.slice(0, 100) + 'ā¦' : result;
|
|
218
|
-
|
|
219
|
-
// if (result.length > 100) {
|
|
220
|
-
// // const logFile = `missions/mission_reports/tool-result-${Date.now()}.log`;
|
|
221
|
-
// // fs.writeFileSync(logFile, result);
|
|
222
|
-
// console.log(`[tool ] ā ${truncated} (full output written to ${logFile})`);
|
|
223
|
-
// } else {
|
|
224
|
-
console.log(`[tool ] ā ${truncated}`);
|
|
225
|
-
// }
|
|
226
|
-
|
|
227
|
-
// messages.push({
|
|
228
|
-
// role: 'tool',
|
|
229
|
-
// tool_call_id: call.id,
|
|
230
|
-
// name: fnName,
|
|
231
|
-
// type: 'function',
|
|
232
|
-
// content: result,
|
|
233
|
-
// });
|
|
165
|
+
console.log(`[tool ] ā ${truncated}`);
|
|
234
166
|
|
|
235
167
|
toolResponses.push({
|
|
236
168
|
role: 'tool',
|
|
@@ -249,7 +181,7 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
249
181
|
const domHtml = await CHROME_TOOL_MAP.get_dom(browser, {
|
|
250
182
|
limit: 100000,
|
|
251
183
|
exclude: true,
|
|
252
|
-
focus: [],
|
|
184
|
+
focus: [],
|
|
253
185
|
}, agentMemory);
|
|
254
186
|
await tokenEstimate('gpt-4o', domHtml);
|
|
255
187
|
// fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
@@ -258,18 +190,9 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
258
190
|
});
|
|
259
191
|
console.log(`[auto] ā DOM size after ${fnName}: ${domHtml.length} chars`);
|
|
260
192
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// if (!hasResponse) {
|
|
265
|
-
// console.error(`ā Tool call ${toolCallId} for ${call.function.name} was not followed by a response`);
|
|
266
|
-
// return false;
|
|
267
|
-
// }
|
|
268
|
-
}
|
|
269
193
|
|
|
270
|
-
|
|
194
|
+
}
|
|
271
195
|
messages.push(msg, ...toolResponses);
|
|
272
|
-
|
|
273
196
|
continue;
|
|
274
197
|
}
|
|
275
198
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testronaut",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"main": "./runner/testronaut.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"testronaut": "./bin/cli.js"
|
|
@@ -25,5 +25,12 @@
|
|
|
25
25
|
"openai": "^5.10.2",
|
|
26
26
|
"playwright": "^1.54.1",
|
|
27
27
|
"prompts": "^2.4.2"
|
|
28
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"cli",
|
|
31
|
+
"testing",
|
|
32
|
+
"automation",
|
|
33
|
+
"node",
|
|
34
|
+
"qa"
|
|
35
|
+
]
|
|
29
36
|
}
|
package/runner/missionLaunch.js
CHANGED
package/runner/testronaut.js
CHANGED
|
@@ -16,38 +16,6 @@ export async function launch(preMissionSetup) {
|
|
|
16
16
|
await runSuite(objectives, preMissionSetup);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
// export async function runMissions({ preMission, mission, postMission }) {
|
|
21
|
-
// const goals = [];
|
|
22
|
-
|
|
23
|
-
// if (preMission) {
|
|
24
|
-
// console.log('\nš§ Running pre-mission setup:', preMission);
|
|
25
|
-
// goals.push({ goal: preMission, label: 'pre-mission' });
|
|
26
|
-
// }
|
|
27
|
-
|
|
28
|
-
// if (mission) {
|
|
29
|
-
// console.log('\nš§ Running main mission:', mission);
|
|
30
|
-
// goals.push({ goal: mission, label: 'mission' });
|
|
31
|
-
// }
|
|
32
|
-
|
|
33
|
-
// if (postMission) {
|
|
34
|
-
// console.log('\nš§ Running post-mission cleanup:', postMission);
|
|
35
|
-
// goals.push({ goal: postMission, label: 'post-mission' });
|
|
36
|
-
// }
|
|
37
|
-
|
|
38
|
-
// console.log('\nš§ Running mission flow:\n', goals);
|
|
39
|
-
|
|
40
|
-
// // for (const { goal, label } of goals) {
|
|
41
|
-
// // console.log(`\nš§ Running ${label}:\n`);
|
|
42
|
-
// const success = await runAgent(goals);
|
|
43
|
-
// if (!success) {
|
|
44
|
-
// console.log(`ā Aborting after failed goal.`);
|
|
45
|
-
// return;
|
|
46
|
-
// }
|
|
47
|
-
// // }
|
|
48
|
-
// console.log('\nā
Mission flow complete.');
|
|
49
|
-
// }
|
|
50
|
-
|
|
51
19
|
export async function runMissions({ preMission, mission, postMission }, missionName) {
|
|
52
20
|
const normalizeToArray = (x) => (Array.isArray(x) ? x : x ? [x] : []);
|
|
53
21
|
|
|
@@ -58,17 +26,14 @@ export async function runMissions({ preMission, mission, postMission }, missionN
|
|
|
58
26
|
const goals = [];
|
|
59
27
|
|
|
60
28
|
if (pre.length) {
|
|
61
|
-
// console.log('\nš§ Running pre-mission setup:', pre);
|
|
62
29
|
goals.push(...pre.map(fn => ({ goal: fn, label: 'pre-mission' })));
|
|
63
30
|
}
|
|
64
31
|
|
|
65
32
|
if (main.length) {
|
|
66
|
-
// console.log('\nš§ Running main mission(s):', main);
|
|
67
33
|
goals.push(...main.map(fn => ({ goal: fn, label: 'mission' })));
|
|
68
34
|
}
|
|
69
35
|
|
|
70
36
|
if (post.length) {
|
|
71
|
-
// console.log('\nš§ Running post-mission cleanup:', post);
|
|
72
37
|
goals.push(...post.map(fn => ({ goal: fn, label: 'post-mission' })));
|
|
73
38
|
}
|
|
74
39
|
|
|
@@ -84,5 +49,4 @@ export async function runMissions({ preMission, mission, postMission }, missionN
|
|
|
84
49
|
}
|
|
85
50
|
console.log('\nā
Mission flow complete.');
|
|
86
51
|
return success;
|
|
87
|
-
|
|
88
52
|
}
|
package/tools/chromeBrowser.js
CHANGED
|
@@ -92,7 +92,7 @@ export class ChromeBrowser {
|
|
|
92
92
|
await el.click();
|
|
93
93
|
await this.page.screenshot({ path: `missions/mission_reports/screenshot_after_click_${Date.now()}.png` });
|
|
94
94
|
await this.page.waitForTimeout(5000);
|
|
95
|
-
|
|
95
|
+
await this.page.screenshot({ path: `screenshot_after_waiting_click_${Date.now()}.png` });
|
|
96
96
|
return `clicked text "${text}"`;
|
|
97
97
|
|
|
98
98
|
}
|
|
@@ -147,11 +147,6 @@ export class ChromeBrowser {
|
|
|
147
147
|
await this.page.waitForSelector(selector, { timeout: 30_000 });
|
|
148
148
|
await this.page.click(selector);
|
|
149
149
|
await this.page.waitForTimeout(delayMs);
|
|
150
|
-
|
|
151
|
-
const expandedDom = await this.page.content();
|
|
152
|
-
// const filename = `missions/mission_reports/expand_menu_dump_${Date.now()}.html`;
|
|
153
|
-
// fs.writeFileSync(filename, expandedDom);
|
|
154
|
-
// console.log(`[expand_menu] ā Dumped DOM after expand click to ${filename}`);
|
|
155
150
|
await this.page.screenshot({ path: `missions/mission_reports/screenshot_expand_menu_${Date.now()}.png` });
|
|
156
151
|
|
|
157
152
|
return `expanded menu using ${selector}`;
|