testronaut 1.0.9 ā 1.0.10
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 +47 -20
- package/core/turnLoop.js +57 -10
- package/package.json +1 -1
- package/runner/testronaut.js +9 -0
- package/tools/chromeBrowser.js +10 -7
- package/tools/generateHtmlReport.js +109 -39
- package/tools/toolSchema.js +18 -0
- package/tools/turnLoopUtils.js +2 -2
package/core/agent.js
CHANGED
|
@@ -6,10 +6,13 @@ export async function runAgent(goals, missionName, maxTurns = 20, ) {
|
|
|
6
6
|
const browser = new ChromeBrowser();
|
|
7
7
|
await browser.start();
|
|
8
8
|
const startTime = Date.now();
|
|
9
|
+
let result;
|
|
9
10
|
|
|
10
11
|
try {
|
|
12
|
+
const missionResults = [];
|
|
11
13
|
for (const goal of goals) {
|
|
12
14
|
// console.log(`\nš Running Agent with goal:\n${goal}\n`);
|
|
15
|
+
const steps = [];
|
|
13
16
|
const messages = [
|
|
14
17
|
{
|
|
15
18
|
role: 'system',
|
|
@@ -29,31 +32,55 @@ export async function runAgent(goals, missionName, maxTurns = 20, ) {
|
|
|
29
32
|
{ role: 'user', content: goal.goal },
|
|
30
33
|
];
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
result = await turnLoop(
|
|
36
|
+
browser,
|
|
37
|
+
messages,
|
|
38
|
+
maxTurns,
|
|
39
|
+
0, /* currentTurn */
|
|
40
|
+
0, /* retryCount */
|
|
41
|
+
null, /* currentStep */
|
|
42
|
+
{
|
|
43
|
+
steps, // š pass in
|
|
44
|
+
missionName, // š pass in (for tagging)
|
|
42
45
|
}
|
|
43
|
-
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
missionResults.push({
|
|
49
|
+
missionName,
|
|
50
|
+
status: result.success ? 'passed' : 'failed',
|
|
51
|
+
steps: JSON.parse(JSON.stringify(steps)), // š deep-clone to avoid refs
|
|
52
|
+
startTime: Date.now() - 1, // set real values if you have them
|
|
53
|
+
endTime: Date.now(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!result.success) {
|
|
57
|
+
console.log('š Agent stopped due to failed goal.\n');
|
|
58
|
+
return missionResults;
|
|
44
59
|
}
|
|
60
|
+
|
|
61
|
+
// if (!result.success) {
|
|
62
|
+
// console.log('š Agent stopped due to failed goal.\n');
|
|
63
|
+
// const endTime = Date.now();
|
|
64
|
+
// return {
|
|
65
|
+
// missionName: missionName,
|
|
66
|
+
// status: 'failed',
|
|
67
|
+
// steps: result.steps,
|
|
68
|
+
// startTime: startTime,
|
|
69
|
+
// endTime: endTime,
|
|
70
|
+
// };
|
|
71
|
+
// }
|
|
45
72
|
}
|
|
46
73
|
|
|
47
74
|
console.log('ā
All goals completed successfully.\n');
|
|
48
|
-
const endTime = Date.now();
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
;
|
|
75
|
+
// const endTime = Date.now();
|
|
76
|
+
return missionResults;
|
|
77
|
+
// return {
|
|
78
|
+
// missionName: missionName,
|
|
79
|
+
// status: 'passed',
|
|
80
|
+
// steps: result.steps,
|
|
81
|
+
// startTime: startTime,
|
|
82
|
+
// endTime: endTime,
|
|
83
|
+
// };
|
|
57
84
|
|
|
58
85
|
} finally {
|
|
59
86
|
await browser.close();
|
package/core/turnLoop.js
CHANGED
|
@@ -19,6 +19,7 @@ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
|
19
19
|
let totalTokensUsed = 0;
|
|
20
20
|
let turnTimestamps = [];
|
|
21
21
|
let shouldBackoff;
|
|
22
|
+
let steps = [];
|
|
22
23
|
|
|
23
24
|
const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool } = {}) => {
|
|
24
25
|
if (skipIfLastTool && skipIfLastTool.includes(messages.at(-1)?.name)) {
|
|
@@ -55,7 +56,16 @@ const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool
|
|
|
55
56
|
});
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
export const turnLoop = async (
|
|
59
|
+
export const turnLoop = async (
|
|
60
|
+
browser,
|
|
61
|
+
messages,
|
|
62
|
+
maxTurns,
|
|
63
|
+
currentTurn = 0,
|
|
64
|
+
retryCount = 0,
|
|
65
|
+
currentStep = {},
|
|
66
|
+
ctx = {} // š { steps, missionName }
|
|
67
|
+
) => {
|
|
68
|
+
const { steps = [], missionName } = ctx;
|
|
59
69
|
let agentMemory = {
|
|
60
70
|
lastMenuExpanded: false
|
|
61
71
|
};
|
|
@@ -63,6 +73,8 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
63
73
|
for (let turn = currentTurn; turn < maxTurns; turn++) {
|
|
64
74
|
console.log(`\nš Turn ${turn + 1}/${maxTurns}`);
|
|
65
75
|
let response;
|
|
76
|
+
// const currentStep = { turn, events: [], result: 'š” In Progress' };
|
|
77
|
+
const currentStep = { turn, events: [], result: 'š” In Progress', missionName }; // š tag it
|
|
66
78
|
try {
|
|
67
79
|
|
|
68
80
|
// Recalculate the rolling window before checking cooldown
|
|
@@ -70,12 +82,15 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
70
82
|
|
|
71
83
|
// Cool off check
|
|
72
84
|
({ totalTokensUsed, turnTimestamps, shouldBackoff } = await tokenUseCoolOff(totalTokensUsed, turnTimestamps));
|
|
73
|
-
if(shouldBackoff) return await turnLoop(browser, messages, maxTurns, turn);
|
|
85
|
+
if(shouldBackoff) return await turnLoop(browser, messages, maxTurns, turn, currentStep);
|
|
74
86
|
|
|
75
87
|
//Tool Validator
|
|
76
88
|
if (!validateAndInsertMissingToolResponses(messages, { insertPlaceholders: true })) {
|
|
77
89
|
console.error('ā Tool call structure invalid and no placeholders inserted.');
|
|
78
|
-
|
|
90
|
+
currentStep.events.push('ā Tool call structure invalid and no placeholders inserted.')
|
|
91
|
+
currentStep.result = 'ā Failure';
|
|
92
|
+
steps.push(currentStep);
|
|
93
|
+
return steps;
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
const unrespondedCalls = messages.filter(
|
|
@@ -92,7 +107,10 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
92
107
|
if (unrespondedCalls.length) {
|
|
93
108
|
console.error('š Detected assistant tool calls without matching tool responses:');
|
|
94
109
|
console.dir(unrespondedCalls, { depth: 5 });
|
|
95
|
-
|
|
110
|
+
currentStep.events.push('š Detected assistant tool calls without matching tool responses')
|
|
111
|
+
currentStep.result = 'ā Failure';
|
|
112
|
+
steps.push(currentStep);
|
|
113
|
+
return steps;
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
|
|
@@ -109,15 +127,21 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
109
127
|
|
|
110
128
|
if (retryCount >= 5) {
|
|
111
129
|
console.error('ā Too many retries. Exiting.');
|
|
112
|
-
|
|
130
|
+
currentStep.events.push('ā Too many retries. Exiting.')
|
|
131
|
+
currentStep.result = 'ā Failure';
|
|
132
|
+
steps.push(currentStep);
|
|
133
|
+
return steps;
|
|
113
134
|
}
|
|
114
135
|
|
|
115
|
-
return await turnLoop(browser, messages, maxTurns, turn, retryCount + 1);
|
|
136
|
+
return await turnLoop(browser, messages, maxTurns, turn, retryCount + 1, currentStep);
|
|
116
137
|
} else if (err.status === 400) {
|
|
117
138
|
console.error('ā Bad request:', err.message);
|
|
118
139
|
// console.log("current tools", toolsSchema);
|
|
119
140
|
// console.log("messages: ", messages)
|
|
120
|
-
|
|
141
|
+
currentStep.events.push(`ā Bad request: ${err.message}`)
|
|
142
|
+
currentStep.result = 'ā Failure';
|
|
143
|
+
steps.push(currentStep);
|
|
144
|
+
return steps;
|
|
121
145
|
} else {
|
|
122
146
|
throw err;
|
|
123
147
|
}
|
|
@@ -127,10 +151,12 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
127
151
|
if (usage) {
|
|
128
152
|
const tokensUsed = usage.total_tokens || 0;
|
|
129
153
|
console.log(`š Token Usage This Turn ā Total: ${tokensUsed}`);
|
|
154
|
+
currentStep.tokensUsed = tokensUsed;
|
|
130
155
|
recordTokenUsage(turnTimestamps, tokensUsed);
|
|
131
156
|
({ turnTimestamps, totalTokensUsed } = pruneOldTokenUsage(turnTimestamps));
|
|
132
157
|
|
|
133
158
|
console.log(`š Running Total Tokens Used (Rolling 60s): ${totalTokensUsed}`);
|
|
159
|
+
currentStep.totalTokensUsed = totalTokensUsed;
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
const msg = response.choices[0].message;
|
|
@@ -145,6 +171,7 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
145
171
|
lastToolName = fnName;
|
|
146
172
|
const args = JSON.parse(call.function.arguments || '{}');
|
|
147
173
|
console.log(`[model] ā ${fnName}`, args);
|
|
174
|
+
currentStep.events.push(`[model] ā ${fnName} ${args}`);
|
|
148
175
|
// console.log('Calling tool:', fnName);
|
|
149
176
|
let result;
|
|
150
177
|
let errorMessage = null;
|
|
@@ -161,9 +188,19 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
161
188
|
}
|
|
162
189
|
|
|
163
190
|
console.log(`[tool ] ā ${fnName} result:`, errorMessage ? 'ā Failed' : 'ā
Success');
|
|
191
|
+
currentStep.events.push(`[tool ] ā ${fnName} result: ${errorMessage ? 'ā Failed' : 'ā
Success'}`)
|
|
164
192
|
const truncated = result.length > 100 ? result.slice(0, 100) + 'ā¦' : result;
|
|
165
193
|
console.log(`[tool ] ā ${truncated}`);
|
|
166
194
|
|
|
195
|
+
if (fnName === 'screenshot') {
|
|
196
|
+
const match = result.match(/screenshot.*?saved at: (.+\.png)/i);
|
|
197
|
+
if (match && match[1]) {
|
|
198
|
+
currentStep.screenshotPath = match[1];
|
|
199
|
+
currentStep.events.push(`š¼ļø Screenshot captured: ${match[1]}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
currentStep.events.push(`[tool ] ā ${truncated}`)
|
|
167
204
|
toolResponses.push({
|
|
168
205
|
role: 'tool',
|
|
169
206
|
tool_call_id: call.id,
|
|
@@ -178,31 +215,41 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
178
215
|
|
|
179
216
|
if (fnName === 'click_text' || fnName === 'click' || fnName === 'expand_menu') {
|
|
180
217
|
console.log(`[auto] ā Injecting DOM after menu expand...`);
|
|
218
|
+
currentStep.events.push(`[auto] ā Injecting DOM after menu expand...`);
|
|
181
219
|
const domHtml = await CHROME_TOOL_MAP.get_dom(browser, {
|
|
182
220
|
limit: 100000,
|
|
183
221
|
exclude: true,
|
|
184
222
|
focus: [],
|
|
185
223
|
}, agentMemory);
|
|
186
224
|
await tokenEstimate('gpt-4o', domHtml);
|
|
187
|
-
// fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
188
225
|
await pushDOMAssistant(browser, messages, agentMemory, {
|
|
189
226
|
skipIfLastTool: ['get_dom', 'check_text']
|
|
190
227
|
});
|
|
191
228
|
console.log(`[auto] ā DOM size after ${fnName}: ${domHtml.length} chars`);
|
|
229
|
+
currentStep.events.push(`[auto] ā DOM size after ${fnName}: ${domHtml.length} chars`)
|
|
192
230
|
}
|
|
193
231
|
|
|
194
232
|
}
|
|
195
233
|
messages.push(msg, ...toolResponses);
|
|
234
|
+
currentStep.result = 'ā
Passed';
|
|
235
|
+
steps.push(currentStep);
|
|
196
236
|
continue;
|
|
197
237
|
}
|
|
198
238
|
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
239
|
+
const finalResponse = finalResponseHandler(msg);
|
|
240
|
+
if (finalResponse !== null) {
|
|
241
|
+
currentStep.events.push(finalResponse.finalMessage);
|
|
242
|
+
currentStep.result = finalResponse.success ? 'ā
Mission Success': 'ā Mission Failure';
|
|
243
|
+
steps.push(currentStep);
|
|
244
|
+
return { success: finalResponse.finalMessage, steps };
|
|
245
|
+
}
|
|
246
|
+
|
|
201
247
|
|
|
202
248
|
await pushDOMAssistant(browser, messages, agentMemory,{
|
|
203
249
|
skipIfLastTool: ['get_dom', 'check_text']
|
|
204
250
|
});
|
|
205
251
|
|
|
206
252
|
console.log(`[auto] ā Injected DOM for next reasoning step`);
|
|
253
|
+
currentStep.events.push(`[auto] ā Injected DOM for next reasoning step`)
|
|
207
254
|
}
|
|
208
255
|
}
|
package/package.json
CHANGED
package/runner/testronaut.js
CHANGED
|
@@ -47,6 +47,15 @@ export async function runMissions({ preMission, mission, postMission }, missionN
|
|
|
47
47
|
console.log(`ā Aborting after failed goal.`);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
+
// console.log("success[0]: ", success[0]);
|
|
51
|
+
// console.log("success[0].steps.length: ", success[0].steps.length);
|
|
52
|
+
// console.log("success[0].steps[success[0].steps.length - 1].result: ", success[0].steps[success[0].steps.length - 1].result)
|
|
53
|
+
const missionStatus = success[0].steps[success[0].steps.length - 1].result;
|
|
54
|
+
// console.log("missionStatus: ", missionStatus)
|
|
55
|
+
if (missionStatus.toLowerCase().includes('failure')) {
|
|
56
|
+
// console.log("we detected the failure!")
|
|
57
|
+
success[0].status = 'failed';
|
|
58
|
+
}
|
|
50
59
|
console.log('\nā
Mission flow complete.');
|
|
51
60
|
return success;
|
|
52
61
|
}
|
package/tools/chromeBrowser.js
CHANGED
|
@@ -90,9 +90,7 @@ export class ChromeBrowser {
|
|
|
90
90
|
if (innerText.includes(text)) {
|
|
91
91
|
await el.scrollIntoViewIfNeeded().catch(() => {});
|
|
92
92
|
await el.click();
|
|
93
|
-
await this.page.screenshot({ path: `missions/mission_reports/screenshot_after_click_${Date.now()}.png` });
|
|
94
93
|
await this.page.waitForTimeout(5000);
|
|
95
|
-
await this.page.screenshot({ path: `screenshot_after_waiting_click_${Date.now()}.png` });
|
|
96
94
|
return `clicked text "${text}"`;
|
|
97
95
|
|
|
98
96
|
}
|
|
@@ -119,7 +117,6 @@ export class ChromeBrowser {
|
|
|
119
117
|
console.log(`[click_text] ā Clicking visible element with bounding box`, box);
|
|
120
118
|
await el.click();
|
|
121
119
|
await this.page.waitForTimeout(delayMs);
|
|
122
|
-
await this.page.screenshot({ path: `missions/mission_reports/screenshot_after_click_${Date.now()}.png` });
|
|
123
120
|
return `clicked visible text "${text}"`;
|
|
124
121
|
}
|
|
125
122
|
}
|
|
@@ -147,11 +144,18 @@ export class ChromeBrowser {
|
|
|
147
144
|
await this.page.waitForSelector(selector, { timeout: 30_000 });
|
|
148
145
|
await this.page.click(selector);
|
|
149
146
|
await this.page.waitForTimeout(delayMs);
|
|
150
|
-
await this.page.screenshot({ path: `missions/mission_reports/screenshot_expand_menu_${Date.now()}.png` });
|
|
151
|
-
|
|
152
147
|
return `expanded menu using ${selector}`;
|
|
153
148
|
}
|
|
154
149
|
|
|
150
|
+
async screenshot({ name = 'screenshot' }) {
|
|
151
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
152
|
+
const screenshotPath = `missions/mission_reports/screenshots/${name}_${timestamp}.png`;
|
|
153
|
+
|
|
154
|
+
await this.page.screenshot({ path: screenshotPath });
|
|
155
|
+
|
|
156
|
+
// ā
Return relative path for HTML report
|
|
157
|
+
return `Screenshot saved at: ./screenshots/${name}_${timestamp}.png`;
|
|
158
|
+
}
|
|
155
159
|
|
|
156
160
|
|
|
157
161
|
async close() {
|
|
@@ -173,7 +177,6 @@ export const CHROME_TOOL_MAP = {
|
|
|
173
177
|
return await browser.click_text(args);
|
|
174
178
|
}
|
|
175
179
|
},
|
|
176
|
-
|
|
177
180
|
expand_menu: (b, args) => b.expand_menu(args),
|
|
178
|
-
|
|
181
|
+
screenshot: (b, args) => b.screenshot(args),
|
|
179
182
|
};
|
|
@@ -1,52 +1,122 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
|
|
3
4
|
export function generateHtmlReport(report, outputPath) {
|
|
4
|
-
const { runId, startTime, endTime,
|
|
5
|
+
const { runId, startTime, endTime, missions = [], summary = {} } = report;
|
|
6
|
+
const durationSec = ((new Date(endTime) - new Date(startTime)) / 1000).toFixed(2);
|
|
7
|
+
|
|
8
|
+
// tiny helpers
|
|
9
|
+
const esc = (s) => String(s ?? '')
|
|
10
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>');
|
|
12
|
+
const badge = (status) =>
|
|
13
|
+
status === 'passed' ? 'ā
Passed' :
|
|
14
|
+
status === 'failed' ? 'ā Failed' : (status || 'ā');
|
|
15
|
+
|
|
16
|
+
const missionBlock = (m, idx) => {
|
|
17
|
+
const mDurationSec = m.endTime && m.startTime
|
|
18
|
+
? ((m.endTime - m.startTime) / 1000).toFixed(2)
|
|
19
|
+
: 'ā';
|
|
5
20
|
|
|
6
|
-
const missionHtml = missions.map(m => {
|
|
7
|
-
const status = m.status?.toUpperCase?.() || 'UNKNOWN';
|
|
8
|
-
const missionName = m.missionName || 'Unnamed';
|
|
9
|
-
const file = m.file || 'N/A';
|
|
10
21
|
const steps = Array.isArray(m.steps) ? m.steps : [];
|
|
22
|
+
const stepItems = steps.map(step => {
|
|
23
|
+
const events = Array.isArray(step.events) ? step.events : [];
|
|
24
|
+
const imgTag = step.screenshotPath
|
|
25
|
+
? `<img src="${esc(step.screenshotPath)}" alt="screenshot turn ${esc(step.turn)}">`
|
|
26
|
+
: '';
|
|
27
|
+
return `
|
|
28
|
+
<details class="step" ${/Mission Success|Passed/.test(step.result||'') ? '' : 'open'}>
|
|
29
|
+
<summary>
|
|
30
|
+
<span class="turn">Turn ${esc((step.turn ?? 0) + 1)}</span>
|
|
31
|
+
<span class="step-result ${/ā
/.test(step.result||'') ? 'ok' : 'bad'}">${esc(step.result || '')}</span>
|
|
32
|
+
<span class="tokens">tokens: ${esc(step.tokensUsed ?? 'ā')} / total: ${esc(step.totalTokensUsed ?? 'ā')}</span>
|
|
33
|
+
</summary>
|
|
34
|
+
<pre class="events">${esc(events.join('\n')) || '(no events)'}</pre>
|
|
35
|
+
${imgTag}
|
|
36
|
+
</details>
|
|
37
|
+
`;
|
|
38
|
+
}).join('');
|
|
11
39
|
|
|
12
40
|
return `
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
${steps.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</div>
|
|
41
|
+
<details class="mission">
|
|
42
|
+
<summary>
|
|
43
|
+
<span class="name">${esc(m.missionName || path.basename(m.file || `mission_${idx+1}`))}</span>
|
|
44
|
+
<span class="status ${m.status === 'passed' ? 'ok' : (m.status === 'failed' ? 'bad' : '')}">${badge(m.status)}</span>
|
|
45
|
+
<span class="meta">steps: ${steps.length} ⢠duration: ${mDurationSec}s ⢠file: ${esc(m.file || '')}</span>
|
|
46
|
+
</summary>
|
|
47
|
+
<div class="steps">
|
|
48
|
+
${stepItems || '<div class="empty">No steps recorded.</div>'}
|
|
49
|
+
</div>
|
|
50
|
+
</details>
|
|
24
51
|
`;
|
|
25
|
-
}
|
|
52
|
+
};
|
|
26
53
|
|
|
27
54
|
const html = `
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
<!DOCTYPE html>
|
|
56
|
+
<html>
|
|
57
|
+
<head>
|
|
58
|
+
<meta charset="UTF-8" />
|
|
59
|
+
<title>Testronaut Report ā ${esc(runId)}</title>
|
|
60
|
+
<style>
|
|
61
|
+
:root {
|
|
62
|
+
--ok: #16a34a;
|
|
63
|
+
--bad: #dc2626;
|
|
64
|
+
--muted: #6b7280;
|
|
65
|
+
--bg: #f8fafc;
|
|
66
|
+
--card: #ffffff;
|
|
67
|
+
--border: #e5e7eb;
|
|
68
|
+
--mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
|
69
|
+
}
|
|
70
|
+
body { margin:0; padding:24px; font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu; background: var(--bg); color:#111827; }
|
|
71
|
+
h1 { margin: 0 0 6px 0; font-size: 20px; }
|
|
72
|
+
.run-meta { color: var(--muted); margin-bottom: 16px; }
|
|
73
|
+
.summary { display:flex; gap:12px; margin: 12px 0 20px; }
|
|
74
|
+
.pill { border:1px solid var(--border); background: var(--card); padding:6px 10px; border-radius:999px; font-size:12px; }
|
|
75
|
+
.pill.ok { border-color: #bbf7d0; background:#ecfdf5; color: var(--ok); }
|
|
76
|
+
.pill.bad { border-color: #fecaca; background:#fef2f2; color: var(--bad); }
|
|
77
|
+
|
|
78
|
+
details { background: var(--card); border:1px solid var(--border); border-radius:10px; margin:10px 0; }
|
|
79
|
+
summary { cursor:pointer; padding:12px 14px; display:flex; align-items:center; gap:12px; }
|
|
80
|
+
.mission > summary { font-weight:600; }
|
|
81
|
+
.name { flex:1; }
|
|
82
|
+
.status.ok { color: var(--ok); }
|
|
83
|
+
.status.bad { color: var(--bad); }
|
|
84
|
+
.meta { color: var(--muted); font-size:12px; }
|
|
85
|
+
|
|
86
|
+
.steps { padding: 0 14px 12px; }
|
|
87
|
+
.step summary { background: #fafafa; border-bottom:1px solid var(--border); border-radius: 10px 10px 0 0; }
|
|
88
|
+
.turn { font-weight:600; }
|
|
89
|
+
.step-result.ok { color: var(--ok); }
|
|
90
|
+
.step-result.bad { color: var(--bad); }
|
|
91
|
+
.tokens { color: var(--muted); font-size:12px; margin-left:auto; }
|
|
92
|
+
.events { background:#0b1020; color:#e5e7eb; padding:12px; border-radius:8px; margin:12px 0 0; font: 12px/1.4 var(--mono); max-height: 280px; overflow:auto; }
|
|
93
|
+
img { display:block; max-width:100%; border:1px solid var(--border); border-radius:8px; margin-top:10px; }
|
|
94
|
+
.empty { color: var(--muted); padding: 12px; }
|
|
95
|
+
</style>
|
|
96
|
+
</head>
|
|
97
|
+
<body>
|
|
98
|
+
<h1>š§āš Testronaut Report</h1>
|
|
99
|
+
<div class="run-meta">
|
|
100
|
+
<div><strong>Run ID:</strong> ${esc(runId)}</div>
|
|
101
|
+
<div><strong>Start:</strong> ${esc(startTime)}</div>
|
|
102
|
+
<div><strong>End:</strong> ${esc(endTime)} ⢠<strong>Duration:</strong> ${durationSec}s</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="summary">
|
|
106
|
+
<div class="pill">Missions: ${esc(summary.totalMissions ?? missions.length)}</div>
|
|
107
|
+
<div class="pill ok">Passed: ${esc(summary.passed ?? missions.filter(m=>m.status==='passed').length)}</div>
|
|
108
|
+
<div class="pill bad">Failed: ${esc(summary.failed ?? missions.filter(m=>m.status==='failed').length)}</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
${missions.map(missionBlock).join('')}
|
|
112
|
+
|
|
113
|
+
</body>
|
|
114
|
+
</html>
|
|
48
115
|
`;
|
|
49
116
|
|
|
50
|
-
|
|
51
|
-
|
|
117
|
+
const out = outputPath ?? path.resolve('missions/mission_reports', `${runId}.html`);
|
|
118
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
119
|
+
fs.writeFileSync(out, html);
|
|
120
|
+
console.log(`š Report generated at: ${out}`);
|
|
121
|
+
return out;
|
|
52
122
|
}
|
package/tools/toolSchema.js
CHANGED
|
@@ -155,7 +155,25 @@ const toolsSchema = [
|
|
|
155
155
|
required: ['chunkIndex', 'totalChunks']
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
type: 'function',
|
|
161
|
+
function: {
|
|
162
|
+
name: 'screenshot',
|
|
163
|
+
description: 'Takes a screenshot of the current page',
|
|
164
|
+
parameters: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
label: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'Optional label for the screenshot filename',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
required: [],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
158
175
|
}
|
|
176
|
+
|
|
159
177
|
|
|
160
178
|
];
|
|
161
179
|
|
package/tools/turnLoopUtils.js
CHANGED
|
@@ -4,13 +4,13 @@ export const finalResponseHandler = (msg) => {
|
|
|
4
4
|
console.log('\nāā FINAL AGENT RESPONSE āāāāāāāāāāāāāāāāāāā');
|
|
5
5
|
console.log(msg.content);
|
|
6
6
|
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
7
|
-
return true;
|
|
7
|
+
return { finalMessage: msg.content, success: true};
|
|
8
8
|
}
|
|
9
9
|
if (final?.startsWith('failure')) {
|
|
10
10
|
console.log('\nāā FINAL AGENT RESPONSE āāāāāāāāāāāāāāāāāāā');
|
|
11
11
|
console.log(msg.content);
|
|
12
12
|
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
13
|
-
return false;
|
|
13
|
+
return { finalMessage: msg.content, success: false};
|
|
14
14
|
}
|
|
15
15
|
return null;
|
|
16
16
|
}
|