@kkeve/suite 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/bin/keve.d.ts +13 -0
- package/dist/bin/keve.js +164 -0
- package/dist/commands/auth-update.d.ts +16 -0
- package/dist/commands/auth-update.js +88 -0
- package/dist/commands/clean.d.ts +10 -0
- package/dist/commands/clean.js +78 -0
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +292 -0
- package/dist/commands/report.d.ts +12 -0
- package/dist/commands/report.js +159 -0
- package/dist/commands/run.d.ts +29 -0
- package/dist/commands/run.js +417 -0
- package/dist/commands/test.d.ts +12 -0
- package/dist/commands/test.js +102 -0
- package/dist/commands/vitest.d.ts +14 -0
- package/dist/commands/vitest.js +166 -0
- package/dist/config.d.ts +109 -0
- package/dist/config.js +291 -0
- package/dist/core/action-log-writer.d.ts +47 -0
- package/dist/{tools/write-file.js → core/action-log-writer.js} +42 -26
- package/dist/core/auth.d.ts +17 -0
- package/dist/core/auth.js +224 -0
- package/dist/core/cdp-resolver.d.ts +5 -0
- package/dist/core/cdp-resolver.js +93 -0
- package/dist/core/generateReport/report.d.ts +19 -0
- package/dist/core/generateReport/report.js +91 -0
- package/dist/core/generateReport/reportData.d.ts +215 -0
- package/dist/core/generateReport/reportData.js +735 -0
- package/dist/core/global-setup.d.ts +6 -0
- package/dist/core/global-setup.js +113 -0
- package/dist/core/keve-aspect.d.ts +54 -0
- package/dist/core/keve-aspect.js +52 -0
- package/dist/core/keve-decorators.d.ts +51 -0
- package/dist/core/keve-decorators.js +95 -0
- package/dist/core/keve-registry.d.ts +38 -0
- package/dist/core/keve-registry.js +25 -0
- package/dist/core/keve-report.d.ts +27 -0
- package/dist/core/keve-report.js +357 -0
- package/dist/core/keve-test.d.ts +36 -0
- package/dist/core/keve-test.js +364 -0
- package/dist/core/learned-actions.d.ts +24 -0
- package/dist/core/learned-actions.js +65 -0
- package/dist/core/pw-config.d.ts +19 -0
- package/dist/core/pw-config.js +180 -0
- package/dist/index.d.ts +8 -23
- package/dist/index.js +36 -43
- package/dist/page-agent/agent.d.ts +129 -0
- package/dist/page-agent/agent.js +585 -0
- package/dist/page-agent/hooks.d.ts +13 -0
- package/dist/page-agent/hooks.js +218 -0
- package/dist/page-agent/tools.d.ts +29 -0
- package/dist/page-agent/tools.js +297 -0
- package/doc/README.md +124 -0
- package/doc/agents/keve-dev-test-agent.md +140 -0
- package/doc/agents/keve-test-agent.md +178 -0
- package/doc/asserts//346/265/213/350/257/225/345/220/204/351/230/266/346/256/265/350/257/264/346/230/216.png +0 -0
- package/doc/keve-agent-demo.html +370 -0
- package/doc/skills/keve-bddcase-gen/SKILL.md +332 -0
- package/doc/skills/keve-bddcase-gen/references/test-case-rules.md +91 -0
- package/doc/skills/keve-bddcase-gen/references/test-cases-example.md +103 -0
- package/doc/skills/keve-component-gen/SKILL.md +255 -0
- package/doc/skills/keve-component-gen/references/component-mock-strategy.md +74 -0
- package/doc/skills/keve-component-gen/references/component-model-template.md +98 -0
- package/doc/skills/keve-component-gen/references/component-test-template.md +100 -0
- package/doc/skills/keve-component-gen/references/vue-test-utils-guide.md +91 -0
- package/doc/skills/keve-e2e-gen/SKILL.md +179 -0
- package/doc/skills/keve-e2e-gen/references/code-analysis-guide.md +208 -0
- package/doc/skills/keve-e2e-gen/references/decisions.md +36 -0
- package/doc/skills/keve-e2e-gen/references/pom-generation-guide.md +140 -0
- package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +236 -0
- package/doc/skills/keve-e2e-gen/references/test-auditor-guide.md +189 -0
- package/doc/skills/keve-e2e-gen/scripts/extract-components.sh +85 -0
- package/doc/skills/keve-e2e-gen/scripts/inject-testids.sh +95 -0
- package/doc/skills/keve-static-analysis/SKILL.md +362 -0
- package/doc/skills/keve-static-analysis/references/codegraph-analysis-guide.md +108 -0
- package/doc/skills/keve-static-analysis/references/git-impact-analysis.md +81 -0
- package/doc/skills/keve-static-analysis/references/lint-fix-guide.md +69 -0
- package/doc/skills/keve-static-analysis/references/runtime-risk-patterns.md +92 -0
- package/doc/skills/keve-static-analysis/references/security-scan-guide.md +32 -0
- package/doc/skills/keve-static-analysis/references/ts-error-patterns.md +98 -0
- package/doc/skills/keve-static-analysis/references/vue-security-guide.md +72 -0
- package/doc/skills/keve-test-eval/SKILL.md +78 -0
- package/doc/skills/keve-test-eval/references/common-fix-guide.md +166 -0
- package/doc/skills/keve-test-eval/references/confidence-data-guide.md +78 -0
- package/doc/skills/keve-test-eval/references/fix-loop-guide.md +67 -0
- package/doc/skills/keve-unit-gen/SKILL.md +203 -0
- package/doc/skills/keve-unit-gen/references/boundary-conditions.md +78 -0
- package/doc/skills/keve-unit-gen/references/unit-mock-strategy.md +71 -0
- package/doc/skills/keve-unit-gen/references/unit-test-template.md +105 -0
- package/package.json +47 -25
- package/dist/agent/factory.d.ts +0 -25
- package/dist/agent/factory.js +0 -99
- package/dist/agent/index.d.ts +0 -3
- package/dist/agent/index.js +0 -7
- package/dist/agent/loader.d.ts +0 -22
- package/dist/agent/loader.js +0 -117
- package/dist/agent/types.d.ts +0 -9
- package/dist/agent/types.js +0 -2
- package/dist/agent-loop/complete-task.d.ts +0 -28
- package/dist/agent-loop/complete-task.js +0 -70
- package/dist/agent-loop/index.d.ts +0 -16
- package/dist/agent-loop/index.js +0 -26
- package/dist/agent-loop/loop.d.ts +0 -42
- package/dist/agent-loop/loop.js +0 -282
- package/dist/agent-loop/runtime.d.ts +0 -40
- package/dist/agent-loop/runtime.js +0 -163
- package/dist/agent-loop/types.d.ts +0 -161
- package/dist/agent-loop/types.js +0 -105
- package/dist/context/index.d.ts +0 -8
- package/dist/context/index.js +0 -29
- package/dist/context/tokenCalculation.d.ts +0 -55
- package/dist/context/tokenCalculation.js +0 -95
- package/dist/context/truncation.d.ts +0 -67
- package/dist/context/truncation.js +0 -142
- package/dist/context/types.d.ts +0 -7
- package/dist/context/types.js +0 -27
- package/dist/llm/OpenAIClient.d.ts +0 -31
- package/dist/llm/OpenAIClient.js +0 -455
- package/dist/llm/constants.d.ts +0 -2
- package/dist/llm/constants.js +0 -6
- package/dist/llm/errors.d.ts +0 -25
- package/dist/llm/errors.js +0 -51
- package/dist/llm/index.d.ts +0 -49
- package/dist/llm/index.js +0 -158
- package/dist/llm/types.d.ts +0 -124
- package/dist/llm/types.js +0 -2
- package/dist/llm/utils.d.ts +0 -29
- package/dist/llm/utils.js +0 -284
- package/dist/skill/frontmatter-parser.d.ts +0 -50
- package/dist/skill/frontmatter-parser.js +0 -152
- package/dist/skill/index.d.ts +0 -12
- package/dist/skill/index.js +0 -17
- package/dist/skill/loader.d.ts +0 -96
- package/dist/skill/loader.js +0 -256
- package/dist/tools/ask-user.d.ts +0 -8
- package/dist/tools/ask-user.js +0 -47
- package/dist/tools/bash.d.ts +0 -47
- package/dist/tools/bash.js +0 -213
- package/dist/tools/edit-file.d.ts +0 -9
- package/dist/tools/edit-file.js +0 -76
- package/dist/tools/glob.d.ts +0 -8
- package/dist/tools/glob.js +0 -101
- package/dist/tools/grep.d.ts +0 -10
- package/dist/tools/grep.js +0 -76
- package/dist/tools/index.d.ts +0 -24
- package/dist/tools/index.js +0 -47
- package/dist/tools/read-files.d.ts +0 -8
- package/dist/tools/read-files.js +0 -81
- package/dist/tools/write-file.d.ts +0 -8
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* page-agent/agent.ts — KevePageAgent
|
|
4
|
+
*
|
|
5
|
+
* Re-Act agent for E2E test goal exploration.
|
|
6
|
+
* Refactored from react-loop.ts to follow PageAgentCore patterns:
|
|
7
|
+
* - MacroTool: merge all tool schemas into one LLM output
|
|
8
|
+
* - History events: step/observation/error stream
|
|
9
|
+
* - Reflection-before-action: evaluation + memory + next_goal + action
|
|
10
|
+
* - Screenshot injected into LLM prompt as multimodal input — LLM self-evaluates visually
|
|
11
|
+
* - No external verification — LLM sees screenshot each step, judges goal state itself
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const agent = new KevePageAgent(page, { maxSteps: 5 });
|
|
15
|
+
* const result = await agent.execute(step, expected, { learnedActionsHint, fnResult });
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.KevePageAgent = void 0;
|
|
52
|
+
exports.reactLoop = reactLoop;
|
|
53
|
+
const llm_1 = require("@ks-data/keve-core/llm");
|
|
54
|
+
const zod_1 = require("zod");
|
|
55
|
+
const fs = __importStar(require("node:fs"));
|
|
56
|
+
const path = __importStar(require("node:path"));
|
|
57
|
+
const hooks_1 = require("./hooks");
|
|
58
|
+
const tools_1 = require("./tools");
|
|
59
|
+
const config_1 = require("../config");
|
|
60
|
+
// ─── System Prompt ────────────────────────────────────────────────────
|
|
61
|
+
const SYSTEM_PROMPT = fs.readFileSync(path.join(__dirname, "system_prompt.md"), "utf-8");
|
|
62
|
+
// ─── KevePageAgent ──────────────────────────────────────────────────────
|
|
63
|
+
class KevePageAgent {
|
|
64
|
+
page;
|
|
65
|
+
maxSteps;
|
|
66
|
+
llm;
|
|
67
|
+
events = [];
|
|
68
|
+
abortController = new AbortController();
|
|
69
|
+
systemPrompt;
|
|
70
|
+
/** Current execute options (set at start of execute(), accessible from hooks) */
|
|
71
|
+
options;
|
|
72
|
+
hooks;
|
|
73
|
+
constructor(page, options) {
|
|
74
|
+
this.page = page;
|
|
75
|
+
this.maxSteps = options?.maxSteps ?? 8;
|
|
76
|
+
this.systemPrompt = options?.customSystemPrompt || SYSTEM_PROMPT;
|
|
77
|
+
this.hooks = options?.hooks;
|
|
78
|
+
const cfg = (0, config_1.loadConfig)();
|
|
79
|
+
this.llm = new llm_1.LLM({
|
|
80
|
+
baseURL: cfg.llm.base_url,
|
|
81
|
+
model: cfg.llm.model,
|
|
82
|
+
apiKey: cfg.llm.api_key || '',
|
|
83
|
+
temperature: 0.1,
|
|
84
|
+
maxRetries: 3,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Stop the current execution */
|
|
88
|
+
stop() {
|
|
89
|
+
this.abortController.abort();
|
|
90
|
+
}
|
|
91
|
+
/** Main Re-Act execute loop */
|
|
92
|
+
async execute(step, expected, options) {
|
|
93
|
+
// Reset state
|
|
94
|
+
this.events = [];
|
|
95
|
+
this.abortController = new AbortController();
|
|
96
|
+
this.options = options;
|
|
97
|
+
const hooks = options?.hooks ?? this.hooks;
|
|
98
|
+
// Link external signal (e.g. per-goal timeout) to internal abort controller
|
|
99
|
+
const externalSignal = options?.signal;
|
|
100
|
+
if (externalSignal) {
|
|
101
|
+
if (externalSignal.aborted) {
|
|
102
|
+
throw new Error('Agent aborted before execution (external signal already aborted)');
|
|
103
|
+
}
|
|
104
|
+
externalSignal.addEventListener('abort', () => {
|
|
105
|
+
this.abortController.abort();
|
|
106
|
+
}, { once: true });
|
|
107
|
+
}
|
|
108
|
+
const maxSteps = options?.maxSteps ?? this.maxSteps;
|
|
109
|
+
const systemPrompt = options?.customSystemPrompt || this.systemPrompt;
|
|
110
|
+
// Inject context/learned hints as initial observations
|
|
111
|
+
const targetUrl = process.env.KEVE_TARGET_URL || '';
|
|
112
|
+
if (targetUrl) {
|
|
113
|
+
this.pushObservation(`Target application URL: ${targetUrl}. Use this URL for navigation. Do NOT construct URLs yourself — use the navigate tool with this URL or read process.env.KEVE_TARGET_URL.`);
|
|
114
|
+
}
|
|
115
|
+
if (options?.learnedActionsHint) {
|
|
116
|
+
this.pushObservation(`Previous discoveries for similar steps:\n${options.learnedActionsHint}`);
|
|
117
|
+
}
|
|
118
|
+
// Step limit warning
|
|
119
|
+
if (maxSteps <= 3) {
|
|
120
|
+
this.pushObservation(`⚠️ Only ${maxSteps} steps allowed. Be efficient.`);
|
|
121
|
+
}
|
|
122
|
+
// ── onBeforeTask hook ──
|
|
123
|
+
await hooks?.onBeforeTask?.(this);
|
|
124
|
+
let stepCount = 0;
|
|
125
|
+
let taskResult;
|
|
126
|
+
try {
|
|
127
|
+
while (true) {
|
|
128
|
+
if (this.abortController.signal.aborted) {
|
|
129
|
+
taskResult = this.buildResult(false, 'Agent stopped', stepCount);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
// ── onBeforeStep hook ──
|
|
133
|
+
await hooks?.onBeforeStep?.(this, stepCount);
|
|
134
|
+
try {
|
|
135
|
+
console.group(`step: ${stepCount}`);
|
|
136
|
+
// ── Observe: get browser state ──
|
|
137
|
+
console.log('\x1b[34m\x1b[1m👀 Observing...\x1b[0m');
|
|
138
|
+
const snapshot = await this.page.ariaSnapshot({ mode: 'ai' });
|
|
139
|
+
const url = this.page.url();
|
|
140
|
+
console.log(` url: ${url.slice(0, 120)} | step ${stepCount + 1}/${maxSteps}`);
|
|
141
|
+
// ── Capture screenshot (used for both LLM multimodal input and artifact save) ──
|
|
142
|
+
let screenshotBuf;
|
|
143
|
+
let screenshotBase64;
|
|
144
|
+
try {
|
|
145
|
+
screenshotBuf = await this.page.screenshot({ type: 'png', timeout: 10000 });
|
|
146
|
+
screenshotBase64 = screenshotBuf.toString('base64');
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
// Retry once with longer timeout (font loading can be slow on some pages)
|
|
150
|
+
try {
|
|
151
|
+
await new Promise(r => setTimeout(r, 500));
|
|
152
|
+
screenshotBuf = await this.page.screenshot({ type: 'png', timeout: 15000 });
|
|
153
|
+
screenshotBase64 = screenshotBuf.toString('base64');
|
|
154
|
+
}
|
|
155
|
+
catch (e2) {
|
|
156
|
+
console.log(`[agent] screenshot capture failed (non-critical): ${e2.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ── Assemble messages (multimodal: text + screenshot) ──
|
|
160
|
+
const userText = this.assembleUserPrompt(step, expected, snapshot, url, stepCount, maxSteps, !!screenshotBase64);
|
|
161
|
+
let userContent;
|
|
162
|
+
if (screenshotBase64) {
|
|
163
|
+
userContent = [
|
|
164
|
+
{ type: 'text', text: userText },
|
|
165
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${screenshotBase64}`, detail: 'high' } },
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
userContent = userText;
|
|
170
|
+
}
|
|
171
|
+
const messages = [
|
|
172
|
+
{ role: 'system', content: systemPrompt },
|
|
173
|
+
{ role: 'user', content: userContent },
|
|
174
|
+
];
|
|
175
|
+
// ── Think + Act: LLM decides ──
|
|
176
|
+
console.log('\x1b[34m\x1b[1m🧠 Thinking...\x1b[0m');
|
|
177
|
+
const macroTool = this.buildMacroTool();
|
|
178
|
+
const llmResult = await this.llm.invoke(messages, macroTool, this.abortController.signal, { toolChoiceName: 'AgentOutput' });
|
|
179
|
+
const macroInput = llmResult.toolCall.args;
|
|
180
|
+
const execResult = llmResult.toolResult;
|
|
181
|
+
const reflection = {
|
|
182
|
+
evaluation: macroInput.evaluation_previous_goal || '',
|
|
183
|
+
memory: macroInput.memory || '',
|
|
184
|
+
nextGoal: macroInput.next_goal || '',
|
|
185
|
+
};
|
|
186
|
+
// Print reflection
|
|
187
|
+
if (reflection.evaluation)
|
|
188
|
+
console.log(`✅: ${reflection.evaluation.slice(0, 150)}`);
|
|
189
|
+
if (reflection.memory)
|
|
190
|
+
console.log(`💾: ${reflection.memory.slice(0, 150)}`);
|
|
191
|
+
if (reflection.nextGoal)
|
|
192
|
+
console.log(`🎯: ${reflection.nextGoal.slice(0, 150)}`);
|
|
193
|
+
const actionName = execResult.toolName;
|
|
194
|
+
const actionInput = macroInput;
|
|
195
|
+
// Save screenshot (reuse buffer already captured for LLM)
|
|
196
|
+
let screenshotPath;
|
|
197
|
+
if (screenshotBuf) {
|
|
198
|
+
try {
|
|
199
|
+
screenshotPath = this.saveScreenshotBuffer(screenshotBuf, stepCount, step);
|
|
200
|
+
}
|
|
201
|
+
catch { /* non-critical */ }
|
|
202
|
+
}
|
|
203
|
+
// Record step event
|
|
204
|
+
if (execResult.error) {
|
|
205
|
+
console.log(`\x1b[31m\x1b[1m${actionName} (error: ${execResult.error.slice(0, 80)})\x1b[0m`);
|
|
206
|
+
}
|
|
207
|
+
else if (execResult.duration !== undefined) {
|
|
208
|
+
console.log(`\x1b[32m\x1b[1m${actionName} executed for ${execResult.duration}ms\x1b[0m ${execResult.output.slice(0, 100)}`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
console.log(`\x1b[32m\x1b[1m${actionName}\x1b[0m ${execResult.output.slice(0, 100)}`);
|
|
212
|
+
}
|
|
213
|
+
const stepEvent = {
|
|
214
|
+
type: 'step',
|
|
215
|
+
stepIndex: stepCount,
|
|
216
|
+
evaluation: reflection.evaluation,
|
|
217
|
+
memory: reflection.memory,
|
|
218
|
+
nextGoal: reflection.nextGoal,
|
|
219
|
+
toolName: actionName,
|
|
220
|
+
toolInput: actionInput,
|
|
221
|
+
toolOutput: execResult.output,
|
|
222
|
+
toolError: execResult.error,
|
|
223
|
+
snapshot,
|
|
224
|
+
screenshotPath,
|
|
225
|
+
};
|
|
226
|
+
this.events.push(stepEvent);
|
|
227
|
+
// If execution had error, add observation
|
|
228
|
+
if (execResult.error) {
|
|
229
|
+
this.pushObservation(`Action "${actionName}" failed: ${execResult.error}. Try a different approach.`);
|
|
230
|
+
}
|
|
231
|
+
// ── Check: if "done", return directly with test conclusion ──
|
|
232
|
+
if (actionName === 'done') {
|
|
233
|
+
// Extract conclusion: 4-level backoff
|
|
234
|
+
let conclusion;
|
|
235
|
+
const verdictField = actionInput?.verdict;
|
|
236
|
+
if (verdictField === 'pass' || verdictField === 'fail' || verdictField === 'blocked') {
|
|
237
|
+
// Level 1: explicit verdict field (preferred, no schema conflict)
|
|
238
|
+
conclusion = verdictField;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const resultField = actionInput?.result;
|
|
242
|
+
if (resultField === 'pass' || resultField === 'fail' || resultField === 'blocked') {
|
|
243
|
+
// Level 2: result field (legacy, may collide with other tools via passthrough)
|
|
244
|
+
conclusion = resultField;
|
|
245
|
+
}
|
|
246
|
+
else if (typeof actionInput?.success === 'boolean') {
|
|
247
|
+
// Level 3: success boolean (legacy)
|
|
248
|
+
conclusion = actionInput.success ? 'pass' : 'fail';
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// Level 4: parse text for verdict keyword (LLM often puts verdict in text)
|
|
252
|
+
// Use includes (not exact match) — text may be a long description
|
|
253
|
+
// IMPORTANT: Check fail/blocked BEFORE pass to avoid "不通过" hitting "通过"
|
|
254
|
+
const textVal = String(actionInput?.text || '').trim().toLowerCase();
|
|
255
|
+
if (textVal.includes('fail') || textVal.includes('failure')
|
|
256
|
+
|| textVal.includes('失败') || textVal.includes('未通过') || textVal.includes('不通过')) {
|
|
257
|
+
conclusion = 'fail';
|
|
258
|
+
}
|
|
259
|
+
else if (textVal.includes('blocked') || textVal.includes('阻塞') || textVal.includes('阻止')) {
|
|
260
|
+
conclusion = 'blocked';
|
|
261
|
+
}
|
|
262
|
+
else if (textVal.includes('pass') || textVal.includes('success') || textVal.includes('ok')
|
|
263
|
+
|| textVal.includes('完成') || textVal.includes('成功') || textVal.includes('通过')
|
|
264
|
+
|| textVal.includes('验证通过') || textVal.includes('符合预期') || textVal.includes('一致')
|
|
265
|
+
|| textVal.includes('匹配') || textVal.includes('确认') || textVal.includes('正确')
|
|
266
|
+
|| textVal.includes('无误') || textVal.includes('生效')) {
|
|
267
|
+
conclusion = 'pass';
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
// text 非空且无否定词 → LLM 写了一段验证结论,大概率是正面
|
|
271
|
+
// text 为空 → 保守判 fail
|
|
272
|
+
conclusion = textVal.length > 0 ? 'pass' : 'fail';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const data = String(actionInput?.text || { pass: 'Goal achieved', fail: 'Expected not achieved', blocked: 'Blocked' }[conclusion]);
|
|
277
|
+
const success = conclusion === 'pass';
|
|
278
|
+
console.log(`\x1b[32m\x1b[1mTask completed\x1b[0m ${conclusion} ${data}`);
|
|
279
|
+
taskResult = this.buildResult(success, data, stepCount, snapshot);
|
|
280
|
+
// Attach conclusion for downstream consumers (keve-report, keve-test)
|
|
281
|
+
taskResult.conclusion = conclusion;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
// LLM call or action failed — record error and continue
|
|
287
|
+
const isAbort = err?.name === 'AbortError' || err?.rawError?.name === 'AbortError';
|
|
288
|
+
if (!isAbort)
|
|
289
|
+
console.error('\x1b[31mTask failed\x1b[0m', err);
|
|
290
|
+
const message = isAbort ? 'Agent stopped' : String(err);
|
|
291
|
+
this.events.push({ type: 'error', message });
|
|
292
|
+
if (!isAbort)
|
|
293
|
+
this.pushObservation(`Error occurred: ${err.message}. Trying to recover.`);
|
|
294
|
+
if (isAbort) {
|
|
295
|
+
taskResult = this.buildResult(false, message, stepCount);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
console.groupEnd();
|
|
301
|
+
// ── onAfterStep hook (runs even on error/abort) ──
|
|
302
|
+
await hooks?.onAfterStep?.(this, this.events);
|
|
303
|
+
}
|
|
304
|
+
stepCount++;
|
|
305
|
+
if (stepCount >= maxSteps) {
|
|
306
|
+
console.error(`\x1b[31mStep count exceeded maximum limit (${maxSteps})\x1b[0m`);
|
|
307
|
+
const finalSnapshot = await this.page.ariaSnapshot();
|
|
308
|
+
taskResult = this.buildResult(false, `Max steps (${maxSteps}) exceeded`, stepCount, finalSnapshot);
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
// Small delay between steps for page stability
|
|
312
|
+
await new Promise(r => setTimeout(r, 300));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
// ── onAfterTask hook — may return partial overrides ──
|
|
317
|
+
const hookResult = await hooks?.onAfterTask?.(this, taskResult);
|
|
318
|
+
if (hookResult && taskResult) {
|
|
319
|
+
Object.assign(taskResult, hookResult);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return taskResult;
|
|
323
|
+
}
|
|
324
|
+
// ─── Internal helpers ──────────────────────────────────────────────
|
|
325
|
+
pushObservation(content) {
|
|
326
|
+
this.events.push({ type: 'observation', content });
|
|
327
|
+
}
|
|
328
|
+
assembleUserPrompt(step, expected, snapshot, url, stepIndex, maxSteps, hasScreenshot) {
|
|
329
|
+
let prompt = '';
|
|
330
|
+
// <agent_state>
|
|
331
|
+
prompt += '<agent_state>\n';
|
|
332
|
+
prompt += '<user_request>\n';
|
|
333
|
+
prompt += `Step: ${step}\n`;
|
|
334
|
+
prompt += `Expected: ${expected}\n`;
|
|
335
|
+
prompt += '</user_request>\n';
|
|
336
|
+
// <prior_execution>: deterministic fn script result (if fn was provided)
|
|
337
|
+
if (this.options?.fnResult && (this.options.fnResult.success || this.options.fnResult.error)) {
|
|
338
|
+
const resultLine = this.options.fnResult.success
|
|
339
|
+
? 'Result: success — all assertions passed'
|
|
340
|
+
: `Result: error — ${this.options.fnResult.error}`;
|
|
341
|
+
const sourceLine = this.options.fnSource
|
|
342
|
+
? `\nSource: ${this.options.fnSource}`
|
|
343
|
+
: '';
|
|
344
|
+
prompt += `<prior_execution>\nA deterministic script was executed BEFORE your Re-Act loop.\n${resultLine}${sourceLine}\n</prior_execution>\n`;
|
|
345
|
+
}
|
|
346
|
+
prompt += '<step_info>\n';
|
|
347
|
+
prompt += `Step ${stepIndex + 1} of ${maxSteps} max steps\n`;
|
|
348
|
+
prompt += `Current URL: ${url}\n`;
|
|
349
|
+
prompt += `Current time: ${new Date().toLocaleString()}\n`;
|
|
350
|
+
prompt += '</step_info>\n';
|
|
351
|
+
prompt += '</agent_state>\n\n';
|
|
352
|
+
// <agent_history>
|
|
353
|
+
prompt += '<agent_history>\n';
|
|
354
|
+
for (const event of this.events) {
|
|
355
|
+
if (event.type === 'step') {
|
|
356
|
+
prompt += `<step_${event.stepIndex + 1}>\n`;
|
|
357
|
+
prompt += `Evaluation of Previous Step: ${event.evaluation}\n`;
|
|
358
|
+
prompt += `Memory: ${event.memory}\n`;
|
|
359
|
+
prompt += `Next Goal: ${event.nextGoal}\n`;
|
|
360
|
+
prompt += `Action: ${event.toolName} → ${event.toolOutput || event.toolError || 'unknown'}\n`;
|
|
361
|
+
prompt += `</step_${event.stepIndex + 1}>\n`;
|
|
362
|
+
}
|
|
363
|
+
else if (event.type === 'observation') {
|
|
364
|
+
prompt += `<sys>${event.content}</sys>\n`;
|
|
365
|
+
}
|
|
366
|
+
// Skip error events in prompt to avoid polluting reasoning
|
|
367
|
+
}
|
|
368
|
+
prompt += '</agent_history>\n\n';
|
|
369
|
+
// <browser_state>
|
|
370
|
+
prompt += '<browser_state>\n';
|
|
371
|
+
prompt += `Current URL: ${url}\n\n`;
|
|
372
|
+
prompt += `Accessibility Tree (YAML):\n\`\`\`yaml\n${snapshot}\n\`\`\`\n`;
|
|
373
|
+
if (hasScreenshot) {
|
|
374
|
+
prompt += '\n<page_screenshot>\nA screenshot of the current page is provided as an image attached to this message. Use it as the primary visual source of truth for what is actually visible on the page. Cross-reference the screenshot with the accessibility tree to make accurate judgments — especially before calling done(success=true) to verify the expected state is truly achieved.\n</page_screenshot>\n';
|
|
375
|
+
}
|
|
376
|
+
prompt += '</browser_state>\n\n';
|
|
377
|
+
return prompt;
|
|
378
|
+
}
|
|
379
|
+
buildMacroTool() {
|
|
380
|
+
const schema = (0, tools_1.packMacroToolSchema)();
|
|
381
|
+
const signal = this.abortController.signal;
|
|
382
|
+
return {
|
|
383
|
+
AgentOutput: {
|
|
384
|
+
description: 'You MUST call this tool every step! Output your reflection and action.',
|
|
385
|
+
inputSchema: schema,
|
|
386
|
+
execute: async (input) => {
|
|
387
|
+
signal.throwIfAborted();
|
|
388
|
+
const toolName = String(input.tool || 'done');
|
|
389
|
+
const toolDef = tools_1.tools.get(toolName);
|
|
390
|
+
if (!toolDef) {
|
|
391
|
+
return { toolName, output: '', error: `Unknown tool: ${toolName}` };
|
|
392
|
+
}
|
|
393
|
+
// Extract only the fields this tool needs from the flat input
|
|
394
|
+
const shape = (0, tools_1.getZodShape)(toolDef.inputSchema);
|
|
395
|
+
const toolInput = {};
|
|
396
|
+
for (const key of Object.keys(shape)) {
|
|
397
|
+
if (input[key] !== undefined) {
|
|
398
|
+
toolInput[key] = input[key];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
console.log(`\x1b[34m\x1b[1mExecuting tool: ${toolName}\x1b[0m`, toolInput);
|
|
402
|
+
try {
|
|
403
|
+
const ctx = { signal };
|
|
404
|
+
const startTime = Date.now();
|
|
405
|
+
const output = await toolDef.execute.bind(this)(toolInput, ctx);
|
|
406
|
+
signal.throwIfAborted();
|
|
407
|
+
const duration = Date.now() - startTime;
|
|
408
|
+
return { toolName, output, duration };
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
return { toolName, output: '', error: err.message || String(err) };
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** Save screenshot buffer to test artifacts (reuses already-captured buffer) */
|
|
418
|
+
saveScreenshotBuffer(buf, stepIndex, stepName) {
|
|
419
|
+
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
420
|
+
const round = process.env.KEVE_ROUND || 'latest';
|
|
421
|
+
const screenshotsDir = path.join(taskDir, 'test-artifacts', `round-${round}`, 'screenshots');
|
|
422
|
+
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
423
|
+
const safeName = stepName.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '_').slice(0, 30);
|
|
424
|
+
const file = path.join(screenshotsDir, `agent-step${stepIndex}-${safeName}-${Date.now()}.png`);
|
|
425
|
+
fs.writeFileSync(file, buf);
|
|
426
|
+
return path.relative(taskDir, file);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Visual element location: uses LLM multimodal to identify element bbox in screenshot.
|
|
430
|
+
* Returns normalized bbox (0-1000 scale) for coordinate-based clicking.
|
|
431
|
+
*/
|
|
432
|
+
async visualLocateElement(screenshotBase64, description) {
|
|
433
|
+
const locateToolSchema = zod_1.z.object({
|
|
434
|
+
bbox: zod_1.z.array(zod_1.z.number()).describe('Bounding box [x1, y1, x2, y2] in 0-1000 normalized coordinates'),
|
|
435
|
+
analysis: zod_1.z.string().describe('Brief description of what was found'),
|
|
436
|
+
});
|
|
437
|
+
const messages = [
|
|
438
|
+
{
|
|
439
|
+
role: 'system',
|
|
440
|
+
content: `You are a UI element locator. Find the described element in the screenshot and return its bounding box.
|
|
441
|
+
|
|
442
|
+
Output JSON with:
|
|
443
|
+
- bbox: [x1, y1, x2, y2] in 0-1000 scale (relative to screenshot size)
|
|
444
|
+
- analysis: brief description
|
|
445
|
+
|
|
446
|
+
If element not found, return empty bbox: [] and explain in analysis.
|
|
447
|
+
|
|
448
|
+
IMPORTANT: Respond in Chinese.`,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
role: 'user',
|
|
452
|
+
content: [
|
|
453
|
+
{ type: 'text', text: `\u8bf7\u5728\u622a\u56fe\u4e2d\u627e\u5230\u4ee5\u4e0b\u5143\u7d20\uff1a${description}\n\n\u8fd4\u56de bbox [x1, y1, x2, y2]\uff0c\u5750\u6807\u8303\u56f4 0-1000\u3002` },
|
|
454
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${screenshotBase64}`, detail: 'high' } },
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
];
|
|
458
|
+
try {
|
|
459
|
+
const result = await this.llm.invoke(messages, {
|
|
460
|
+
LocateResult: {
|
|
461
|
+
description: 'Return the bounding box of the located element',
|
|
462
|
+
inputSchema: locateToolSchema,
|
|
463
|
+
execute: async (args) => args,
|
|
464
|
+
},
|
|
465
|
+
}, this.abortController.signal, { toolChoiceName: 'LocateResult' });
|
|
466
|
+
const parsed = result.toolCall.args;
|
|
467
|
+
const bbox = parsed.bbox || [];
|
|
468
|
+
if (bbox.length === 4 && bbox.every(v => typeof v === 'number' && !isNaN(v) && v >= 0 && v <= 1000) && bbox[2] > bbox[0] && bbox[3] > bbox[1]) {
|
|
469
|
+
return { found: true, bbox, analysis: parsed.analysis };
|
|
470
|
+
}
|
|
471
|
+
return { found: false, analysis: parsed.analysis || 'LLM returned invalid bbox' };
|
|
472
|
+
}
|
|
473
|
+
catch (e) {
|
|
474
|
+
return { found: false, analysis: `LLM invoke error: ${e.message}` };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/** Build final result */
|
|
478
|
+
buildResult(success, data, stepCount, finalSnapshot) {
|
|
479
|
+
const snapshot = finalSnapshot || '';
|
|
480
|
+
if (!success) {
|
|
481
|
+
this.events.push({ type: 'error', message: data });
|
|
482
|
+
}
|
|
483
|
+
// Collect screenshot paths from step events
|
|
484
|
+
const screenshotPaths = this.events
|
|
485
|
+
.filter((e) => e.type === 'step')
|
|
486
|
+
.filter(e => e.screenshotPath)
|
|
487
|
+
.map(e => e.screenshotPath);
|
|
488
|
+
return { success, data, events: this.events, finalSnapshot: snapshot, /* refinePatch, */ agentScreenshots: screenshotPaths.length ? screenshotPaths : undefined };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
exports.KevePageAgent = KevePageAgent;
|
|
492
|
+
/**
|
|
493
|
+
* @deprecated Use `new KevePageAgent(page).execute(step, expected, options)` directly.
|
|
494
|
+
* Drop-in replacement for the old reactLoop function.
|
|
495
|
+
* Creates a KevePageAgent, runs it, and maps the result.
|
|
496
|
+
*/
|
|
497
|
+
async function reactLoop(page, step, expected, options = {}) {
|
|
498
|
+
const agent = new KevePageAgent(page, {
|
|
499
|
+
maxSteps: options.maxSteps ?? 20,
|
|
500
|
+
hooks: {
|
|
501
|
+
onAfterTask: async (_agent, result) => {
|
|
502
|
+
const partial = {};
|
|
503
|
+
// 1. Diagnostic hints for failures
|
|
504
|
+
if (!result.success) {
|
|
505
|
+
try {
|
|
506
|
+
partial.diagnosticHints = await (0, hooks_1.extractDiagnosticHints)(_agent.page, result);
|
|
507
|
+
}
|
|
508
|
+
catch { /* non-critical */ }
|
|
509
|
+
}
|
|
510
|
+
// 2. Script refine for successes — 已注释:产出无下游消费且不受 keveGoal 超时控制
|
|
511
|
+
// if (result.success) {
|
|
512
|
+
// const opts = _agent.options;
|
|
513
|
+
// if (opts?.specFilePath && opts?.fnSource) {
|
|
514
|
+
// try {
|
|
515
|
+
// const reactActions = result.events
|
|
516
|
+
// .filter((e): e is AgentStepEvent => e.type === 'step')
|
|
517
|
+
// .filter(e => e.toolName !== 'done')
|
|
518
|
+
// .map(e => ({ tool: e.toolName, role: e.toolInput?.role, name: e.toolInput?.name, url: e.toolInput?.url, text: e.toolInput?.text, key: e.toolInput?.key, evaluation: e.evaluation }));
|
|
519
|
+
// const patch = await scriptRefine(_agent.llm, {
|
|
520
|
+
// specFilePath: opts.specFilePath,
|
|
521
|
+
// step, expected,
|
|
522
|
+
// testTitle: step,
|
|
523
|
+
// order: result.events.filter((e): e is AgentStepEvent => e.type === 'step').length,
|
|
524
|
+
// fnSource: opts.fnSource,
|
|
525
|
+
// gap: opts.contextHint || '',
|
|
526
|
+
// reactActions,
|
|
527
|
+
// });
|
|
528
|
+
// if (patch) partial.refinePatch = patch;
|
|
529
|
+
// } catch { /* non-critical */ }
|
|
530
|
+
// }
|
|
531
|
+
// }
|
|
532
|
+
return Object.keys(partial).length ? partial : undefined;
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
let result;
|
|
537
|
+
try {
|
|
538
|
+
result = await agent.execute(step, expected, {
|
|
539
|
+
learnedActionsHint: options.learnedActionsHint,
|
|
540
|
+
specFilePath: options.specFilePath,
|
|
541
|
+
fnSource: options.fnSource,
|
|
542
|
+
fnResult: options.fnResult,
|
|
543
|
+
signal: options.signal,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
catch (execErr) {
|
|
547
|
+
const msg = execErr?.message || String(execErr);
|
|
548
|
+
console.log(`[agent] execute interrupted: ${msg.slice(0, 200)}`);
|
|
549
|
+
const partialEvents = agent.events.filter((e) => e.type === 'step');
|
|
550
|
+
result = {
|
|
551
|
+
success: false,
|
|
552
|
+
message: `Agent interrupted: ${msg.slice(0, 100)}`,
|
|
553
|
+
stepCount: partialEvents.length,
|
|
554
|
+
finalSnapshot: '',
|
|
555
|
+
events: agent.events,
|
|
556
|
+
agentScreenshots: agent.events
|
|
557
|
+
.filter((e) => e.type === 'step')
|
|
558
|
+
.filter(e => e.screenshotPath)
|
|
559
|
+
.map(e => e.screenshotPath),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const actions = result.events
|
|
563
|
+
.filter((e) => e.type === 'step')
|
|
564
|
+
.map(e => ({
|
|
565
|
+
step: e.stepIndex,
|
|
566
|
+
action: { tool: e.toolName, ...e.toolInput },
|
|
567
|
+
toolOutput: e.toolOutput,
|
|
568
|
+
snapshot: e.snapshot,
|
|
569
|
+
evaluation: e.evaluation,
|
|
570
|
+
memory: e.memory,
|
|
571
|
+
nextGoal: e.nextGoal,
|
|
572
|
+
result: e.toolError ? 'error' : 'ok',
|
|
573
|
+
error: e.toolError,
|
|
574
|
+
source: 'react',
|
|
575
|
+
screenshotPath: e.screenshotPath,
|
|
576
|
+
}));
|
|
577
|
+
return {
|
|
578
|
+
actions,
|
|
579
|
+
expectedMet: result.success,
|
|
580
|
+
conclusion: result.conclusion,
|
|
581
|
+
finalSnapshot: result.finalSnapshot,
|
|
582
|
+
// refinePatch: result.refinePatch, // 已注释:scriptRefine 已禁用
|
|
583
|
+
agentScreenshots: result.agentScreenshots,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentResult } from './agent';
|
|
2
|
+
/**
|
|
3
|
+
* diagnostic.ts — Extract diagnostic hints from agent result + page state
|
|
4
|
+
*
|
|
5
|
+
* Called in onAfterTask hook to generate human-readable failure reasons.
|
|
6
|
+
* Inspects the agent's action history, final snapshot, and live page DOM
|
|
7
|
+
* to produce actionable diagnostic hints.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Extract diagnostic hints from agent result and current page state.
|
|
11
|
+
* Only meaningful when result.success === false.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractDiagnosticHints(page: import('@playwright/test').Page, result: AgentResult): Promise<string[]>;
|