@capagents/forgeqe 1.1.2 → 1.1.3
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/HOW_TO_USE.md +5 -5
- package/README.md +3 -0
- package/browserstack.example.yaml +14 -0
- package/docs/security-data-flow.html +154 -86
- package/docs/user-guide.html +305 -131
- package/forgeqe.example.yaml +13 -1
- package/package.json +5 -2
- package/py/forgeqe_engine/service.py +1 -0
- package/src/auth/signin.ts +2 -2
- package/src/bi.ts +61 -46
- package/src/cache.ts +92 -0
- package/src/capabilities/guide.ts +322 -140
- package/src/capabilities/registry.ts +3 -3
- package/src/cli/help.ts +8 -2
- package/src/cli/index.ts +35 -5
- package/src/config.ts +130 -7
- package/src/heal/loop.ts +3 -1
- package/src/init.ts +18 -0
- package/src/mcp/tools.ts +26 -4
- package/src/monitor/run.ts +3 -1
- package/src/run.ts +4 -2
- package/src/suite/run.ts +3 -1
- package/testmu.example.yaml +14 -0
|
@@ -61,11 +61,11 @@ export const CAPABILITIES: Capability[] = [
|
|
|
61
61
|
area: "Browser",
|
|
62
62
|
title: "Run any goal in a real browser",
|
|
63
63
|
summary:
|
|
64
|
-
"Carries out a plain-language goal in
|
|
64
|
+
"Carries out a plain-language goal in a real browser one step at a time and verifies the outcome, with screenshots, a report, and the steps it took. The browser comes from forgeqe.yaml: local Chrome or Chromium by default, or BrowserStack or TestMu AI/LambdaTest when configured. Page memory replays steps that already worked on a site, so repeat runs are faster and cheaper.",
|
|
65
65
|
tools: ["browser_run", "run_details", "page_memory"],
|
|
66
66
|
examples: ["Go to booking.com and search hotels in Mumbai for next weekend", "What does ForgeQE know about the checkout page?"],
|
|
67
67
|
requires: [{ kind: "live_model" }],
|
|
68
|
-
limits: ["Web applications
|
|
68
|
+
limits: ["Web applications only: local Chrome or Chromium, or configured remote web grids such as BrowserStack and TestMu AI/LambdaTest; not native mobile or desktop applications."],
|
|
69
69
|
docs: [
|
|
70
70
|
{ doc: "HOW_TO_USE.md", heading: "4. Without the shell" },
|
|
71
71
|
{ doc: "HOW_TO_USE.md", heading: "5. Page memory" },
|
|
@@ -345,7 +345,7 @@ export const CAPABILITIES: Capability[] = [
|
|
|
345
345
|
* Only add what is certain.
|
|
346
346
|
*/
|
|
347
347
|
export const OUT_OF_SCOPE: { what: string; instead?: string }[] = [
|
|
348
|
-
{ what: "Native mobile apps (iOS, Android) and desktop applications", instead: "web applications in Chrome or Chromium" },
|
|
348
|
+
{ what: "Native mobile apps (iOS, Android) and desktop applications", instead: "web applications in local Chrome or Chromium, or configured remote web grids such as BrowserStack and TestMu AI/LambdaTest" },
|
|
349
349
|
{ what: "Load, stress, or soak testing with many simultaneous users", instead: "per-page Core Web Vitals with budgets" },
|
|
350
350
|
{ what: "Penetration testing or dynamic security scanning", instead: "security findings in pull request reviews" },
|
|
351
351
|
]
|
package/src/cli/help.ts
CHANGED
|
@@ -17,21 +17,25 @@ Usage:
|
|
|
17
17
|
[--repo DIR] [--no-code]
|
|
18
18
|
forgeqe run "<request>" [--profile name] [--url URL] [--headed] [--plain] [--max-steps N]
|
|
19
19
|
[--repo DIR] [--no-code] [--replay MODE] [--no-memory] [--attempts N] [--code-model provider/model]
|
|
20
|
+
[--browser-provider local|browserstack|testmu]
|
|
20
21
|
ForgeQE's agent works out what the request needs: a browser goal,
|
|
21
22
|
test cases from Jira, Azure DevOps, Xray or files, a test, a pipeline
|
|
22
|
-
forgeqe run --direct "<goal>" [--out DIR] [--skill NAME] ... one browser goal, no agent
|
|
23
|
+
forgeqe run --direct "<goal>" [--out DIR] [--skill NAME] [--browser-provider local|browserstack|testmu] ... one browser goal, no agent
|
|
23
24
|
forgeqe suite <source...> [--parallel N] [--repo DIR] [--no-code] [--no-write-back] [--out DIR]
|
|
24
25
|
[--profile name] [--url URL] [--headed] [--plain] [--attempts N] [--skill NAME]
|
|
26
|
+
[--browser-provider local|browserstack|testmu]
|
|
25
27
|
run test cases the same way every time (what pipelines call)
|
|
26
28
|
forgeqe cases <source...> [--goal] list the test cases a source gives (--goal: the goal each runs)
|
|
27
29
|
forgeqe code [run-dir] [--repo DIR] [--attempts N] [--code-model provider/model]
|
|
28
30
|
write a test for a run (default: the latest) with Scribe
|
|
29
31
|
forgeqe heal ["<test command>"] [--repo DIR] [--attempts N] [--code-model provider/model] [--headed] [--plain]
|
|
32
|
+
[--browser-provider local|browserstack|testmu]
|
|
30
33
|
run the repo's tests and heal the ones the app has outgrown
|
|
31
34
|
forgeqe monitor [source...] [--every 15m] [--once] [--replay MODE] [--parallel N] [--out DIR] [--verbose] [--skill NAME]
|
|
35
|
+
[--browser-provider local|browserstack|testmu]
|
|
32
36
|
check flows on a schedule and alert Teams or email when one breaks
|
|
33
37
|
forgeqe monitor --test-alerts send a sample alert to check the Teams and email settings
|
|
34
|
-
forgeqe mcp [--repo DIR] [--profile name] [--url URL] [--headed]
|
|
38
|
+
forgeqe mcp [--repo DIR] [--profile name] [--url URL] [--headed] [--browser-provider local|browserstack|testmu]
|
|
35
39
|
ForgeQE's tools as an MCP server on stdio, for other agents
|
|
36
40
|
forgeqe mcp list the Azure DevOps, Jira and other MCP servers ForgeQE's agent gets
|
|
37
41
|
forgeqe bi [build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR]
|
|
@@ -52,6 +56,7 @@ Usage:
|
|
|
52
56
|
forgeqe visual approve [run-dir] [--page a,b] [--yes]
|
|
53
57
|
make the run's screenshots the new baselines (an intended change)
|
|
54
58
|
forgeqe explore <story> [--url URL] [--charters N] [--roles a,b] [--focus TEXT] [--yes] [--file-bugs]
|
|
59
|
+
[--browser-provider local|browserstack|testmu]
|
|
55
60
|
exploratory testing of a story (ado:123, SHOP-12, a link, or its text):
|
|
56
61
|
charters beyond the scripted cases, defects confirmed by a second run,
|
|
57
62
|
proposed test cases, bugs for confirmed defects; exits 1 on a defect
|
|
@@ -104,6 +109,7 @@ page with ranked locators and a confidence %, plus the steps of each goal's last
|
|
|
104
109
|
--replay verify replay a known goal without the LLM, then one LLM step checks the result (default)
|
|
105
110
|
--replay trust replay and finish without the LLM when every step works
|
|
106
111
|
--replay off never replay; still record pages
|
|
112
|
+
--browser-provider pick which configured browser provider runs this command's browser work
|
|
107
113
|
|
|
108
114
|
ForgeQE's agent (forgeqe start, forgeqe run) runs on the agent runtime with the active profile's model (or code.model) and
|
|
109
115
|
calls ForgeQE's tools: browser runs, reading and running test cases, writing tests and running them, page memory, pushing
|
package/src/cli/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { goalFor, loadCases } from "../cases/index.ts"
|
|
|
10
10
|
import { codeSkipReason, latestRunDir, runCodeAgent } from "../code/index.ts"
|
|
11
11
|
import { detailFrom } from "../detail.ts"
|
|
12
12
|
import { ensureOpencode, INSTALL_HINT } from "../code/runtime.ts"
|
|
13
|
-
import { loadConfig, parseDuration, replayMode, selectProfile, userConfigDir, type AppConfig } from "../config.ts"
|
|
13
|
+
import { browserProvider, loadConfig, parseDuration, replayMode, selectProfile, userConfigDir, type AppConfig, type BrowserProvider } from "../config.ts"
|
|
14
14
|
import { runHeal } from "../heal/loop.ts"
|
|
15
15
|
import { formatHeal } from "../heal/report.ts"
|
|
16
16
|
import { backgroundMonitors, stopBackgroundMonitors } from "../monitor/background.ts"
|
|
@@ -62,6 +62,11 @@ const LOGGED_COMMANDS = new Set([...AGENT_COMMANDS, "mcp", "login", "setup", "bi
|
|
|
62
62
|
/** How much of the agents' work to print: brief unless --verbose. */
|
|
63
63
|
const detailOf = (flags: Flags) => detailFrom(boolFlag(flags, "verbose"))
|
|
64
64
|
|
|
65
|
+
function browserProviderFlag(flags: Flags): BrowserProvider | undefined {
|
|
66
|
+
const value = strFlag(flags, "browser-provider")
|
|
67
|
+
return value ? browserProvider(value) : undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
function debugRequested(flags: Flags): boolean {
|
|
66
71
|
return boolFlag(flags, "debug") || /^(1|true|yes|on)$/i.test(process.env.FORGEQE_DEBUG || "")
|
|
67
72
|
}
|
|
@@ -212,7 +217,7 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
212
217
|
const cfg = loadConfig(configOpts(flags))
|
|
213
218
|
const trends = positionals[1] === "trends"
|
|
214
219
|
const color = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
|
|
215
|
-
|
|
220
|
+
const ctx: ToolContext = { config: cfg, cwd: process.cwd(), session: "once", emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)) }
|
|
216
221
|
const out = (await TOOLS.find((t) => t.name === (trends ? "quality_trends" : "quality_report"))!.run(trends ? { page: positionals[2] } : { run_dir: positionals[1] }, ctx)) as { budget_exceeded?: unknown[]; pages?: { slower: unknown[]; newly_failing_rules: unknown[] }[] }
|
|
217
222
|
return out.budget_exceeded?.length || out.pages?.some((p) => p.slower.length || p.newly_failing_rules.length) ? 1 : 0
|
|
218
223
|
}
|
|
@@ -377,7 +382,7 @@ async function cmdApi(args: string[], flags: Flags): Promise<number> {
|
|
|
377
382
|
const cfg = loadConfig(configOpts(flags))
|
|
378
383
|
const action = args[0] || "list"
|
|
379
384
|
const color = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
|
|
380
|
-
|
|
385
|
+
const ctx: ToolContext = { config: cfg, cwd: process.cwd(), session: "once", emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)) }
|
|
381
386
|
const tool = (name: string) => TOOLS.find((t) => t.name === name)!
|
|
382
387
|
const csv = (name: string) => (strFlag(flags, name) || "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
383
388
|
switch (action) {
|
|
@@ -454,7 +459,17 @@ async function cmdExplore(args: string[], flags: Flags): Promise<number> {
|
|
|
454
459
|
process.once("SIGINT", onSigint)
|
|
455
460
|
try {
|
|
456
461
|
const ask = process.stdin.isTTY && !boolFlag(flags, "yes") ? terminalAsk(controller.signal) : undefined
|
|
457
|
-
const ctx: ToolContext = {
|
|
462
|
+
const ctx: ToolContext = {
|
|
463
|
+
config: cfg,
|
|
464
|
+
cwd: process.cwd(),
|
|
465
|
+
session: "once",
|
|
466
|
+
signal: controller.signal,
|
|
467
|
+
headed: boolFlag(flags, "headed"),
|
|
468
|
+
browserProvider: browserProviderFlag(flags),
|
|
469
|
+
maxSteps: maxStepsFlag(flags),
|
|
470
|
+
emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)),
|
|
471
|
+
...(ask ? { ask } : {}),
|
|
472
|
+
}
|
|
458
473
|
const roles = (strFlag(flags, "roles") || "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
459
474
|
const out = (await TOOLS.find((t) => t.name === "explore_story")!.run(
|
|
460
475
|
{ story, url: strFlag(flags, "url"), charters: Number(strFlag(flags, "charters")) || undefined, ...(roles.length ? { roles } : {}), focus: strFlag(flags, "focus"), file_bugs: boolFlag(flags, "file-bugs") },
|
|
@@ -913,6 +928,7 @@ async function runAgent(cfg: AppConfig, flags: Flags, text: string, maxSteps: nu
|
|
|
913
928
|
signal: controller.signal,
|
|
914
929
|
context: () => ({
|
|
915
930
|
headed: boolFlag(flags, "headed"),
|
|
931
|
+
browserProvider: browserProviderFlag(flags),
|
|
916
932
|
maxSteps,
|
|
917
933
|
url: strFlag(flags, "url"),
|
|
918
934
|
...memoryFlags(flags),
|
|
@@ -973,7 +989,17 @@ async function cmdMcp(args: string[], flags: Flags): Promise<number> {
|
|
|
973
989
|
const repo = repoFlag(flags)
|
|
974
990
|
const cwd = repo || process.cwd()
|
|
975
991
|
const log = toolPrinter(false, (line) => process.stderr.write(`${line}\n`), detailOf(flags))
|
|
976
|
-
const context = (): ToolContext => ({
|
|
992
|
+
const context = (): ToolContext => ({
|
|
993
|
+
config: cfg,
|
|
994
|
+
cwd,
|
|
995
|
+
headed: boolFlag(flags, "headed"),
|
|
996
|
+
browserProvider: browserProviderFlag(flags),
|
|
997
|
+
maxSteps: maxStepsFlag(flags),
|
|
998
|
+
url: strFlag(flags, "url"),
|
|
999
|
+
...memoryFlags(flags),
|
|
1000
|
+
emit: log,
|
|
1001
|
+
session: "server",
|
|
1002
|
+
})
|
|
977
1003
|
const server = createMcpServer({ tools: TOOLS, context, instructions: () => environmentNotes(context()), version: packageVersion() })
|
|
978
1004
|
process.stderr.write(`ForgeQE MCP server on stdio (${TOOLS.length} tools, working in ${cwd})\n`)
|
|
979
1005
|
await serveStdio(server)
|
|
@@ -988,6 +1014,7 @@ async function runGoal(cfg: AppConfig, flags: Flags, goal: string, maxSteps: num
|
|
|
988
1014
|
const { result, files } = await executeRun({
|
|
989
1015
|
config: cfg,
|
|
990
1016
|
goal,
|
|
1017
|
+
browserProvider: browserProviderFlag(flags),
|
|
991
1018
|
url: strFlag(flags, "url"),
|
|
992
1019
|
headed: boolFlag(flags, "headed"),
|
|
993
1020
|
maxSteps,
|
|
@@ -1129,6 +1156,7 @@ async function cmdHeal(args: string[], flags: Flags): Promise<number> {
|
|
|
1129
1156
|
repo,
|
|
1130
1157
|
command,
|
|
1131
1158
|
outDir: strFlag(flags, "out"),
|
|
1159
|
+
browserProvider: browserProviderFlag(flags),
|
|
1132
1160
|
headed: boolFlag(flags, "headed"),
|
|
1133
1161
|
maxSteps: maxStepsFlag(flags),
|
|
1134
1162
|
url: strFlag(flags, "url"),
|
|
@@ -1200,6 +1228,7 @@ async function cmdMonitor(refs: string[], flags: Flags): Promise<number> {
|
|
|
1200
1228
|
config: cfg,
|
|
1201
1229
|
checks,
|
|
1202
1230
|
once,
|
|
1231
|
+
browserProvider: browserProviderFlag(flags),
|
|
1203
1232
|
headed: boolFlag(flags, "headed"),
|
|
1204
1233
|
url: strFlag(flags, "url"),
|
|
1205
1234
|
skills: skillFlags(flags),
|
|
@@ -1238,6 +1267,7 @@ async function cmdSuite(refs: string[], flags: Flags): Promise<number> {
|
|
|
1238
1267
|
config: cfg,
|
|
1239
1268
|
cases,
|
|
1240
1269
|
repo,
|
|
1270
|
+
browserProvider: browserProviderFlag(flags),
|
|
1241
1271
|
parallel,
|
|
1242
1272
|
outDir: strFlag(flags, "out"),
|
|
1243
1273
|
writeBack: boolFlag(flags, "no-write-back") ? false : undefined,
|
package/src/config.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { ragConfig, type RagConfig } from "./cases/rag.ts"
|
|
|
18
18
|
|
|
19
19
|
export type BrowserProvider = "local" | "browserstack" | "testmu"
|
|
20
20
|
export type RemoteBrowserProvider = Exclude<BrowserProvider, "local">
|
|
21
|
+
export type BrowserRemoteConfigs = Partial<Record<RemoteBrowserProvider, BrowserRemoteConfig>>
|
|
22
|
+
export type BrowserProviderFiles = Partial<Record<RemoteBrowserProvider, string>>
|
|
21
23
|
|
|
22
24
|
export type BrowserRemoteConfig = {
|
|
23
25
|
provider: RemoteBrowserProvider
|
|
@@ -47,6 +49,8 @@ export type BrowserConfig = {
|
|
|
47
49
|
allowedHosts: string[]
|
|
48
50
|
startUrl?: string
|
|
49
51
|
remote?: BrowserRemoteConfig
|
|
52
|
+
remotes: BrowserRemoteConfigs
|
|
53
|
+
providerFiles: BrowserProviderFiles
|
|
50
54
|
/** ForgeQE overlay in a visible browser: edge glow, a bar with the current step, the person's input locked. */
|
|
51
55
|
overlay: boolean
|
|
52
56
|
/** A second model call checks the finished run against the goal, and its verdict can fail a run the agent completed. */
|
|
@@ -59,6 +63,20 @@ export type MemoryConfig = {
|
|
|
59
63
|
replay: ReplayMode
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
export type CacheConfig = {
|
|
67
|
+
enabled: boolean
|
|
68
|
+
/** Redis connection string, e.g. redis://127.0.0.1:6379. */
|
|
69
|
+
redisUrl: string
|
|
70
|
+
/** Prefix for every Redis key this ForgeQE workspace writes. */
|
|
71
|
+
keyPrefix: string
|
|
72
|
+
/** Default TTL for cached JSON payloads, in seconds. */
|
|
73
|
+
defaultTtlSeconds: number
|
|
74
|
+
/** TTL for BI external dataset exports, in seconds. */
|
|
75
|
+
biTtlSeconds: number
|
|
76
|
+
/** Socket connect timeout for Redis, in milliseconds. */
|
|
77
|
+
connectTimeoutMs: number
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
function runtimeBin(value: unknown, base: string): string | undefined {
|
|
63
81
|
if (!value) return undefined
|
|
64
82
|
const s = String(value)
|
|
@@ -330,6 +348,8 @@ export type AppConfig = {
|
|
|
330
348
|
configPath?: string
|
|
331
349
|
/** `mcp:` in forgeqe.yaml: more MCP servers for ForgeQE's agent, by name. */
|
|
332
350
|
mcp?: Record<string, ExtraMcpServer>
|
|
351
|
+
/** Optional Redis-backed cache for deterministic external data fetches. */
|
|
352
|
+
cache?: CacheConfig
|
|
333
353
|
activeProfile: string
|
|
334
354
|
profiles: Record<string, LlmProfile>
|
|
335
355
|
llm: LlmProfile
|
|
@@ -390,6 +410,22 @@ function domainConfig(raw: unknown): DomainConfig | undefined {
|
|
|
390
410
|
return { industry, regulations, ...(d.notes ? { notes: String(d.notes).trim() } : {}) }
|
|
391
411
|
}
|
|
392
412
|
|
|
413
|
+
function cacheConfig(raw: unknown): CacheConfig | undefined {
|
|
414
|
+
const c = asRecord(raw)
|
|
415
|
+
const asked = raw !== undefined || Boolean(process.env.FORGEQE_REDIS_URL)
|
|
416
|
+
if (!asked || c.enabled === false) return undefined
|
|
417
|
+
const redisUrl = String(c.redis_url || c.url || process.env.FORGEQE_REDIS_URL || "").trim()
|
|
418
|
+
if (!redisUrl) throw new Error('cache.redis_url is required when cache is enabled. Example: cache: { redis_url: "redis://127.0.0.1:6379" }')
|
|
419
|
+
return {
|
|
420
|
+
enabled: true,
|
|
421
|
+
redisUrl,
|
|
422
|
+
keyPrefix: String(c.key_prefix || "forgeqe").trim() || "forgeqe",
|
|
423
|
+
defaultTtlSeconds: positiveInt(c.default_ttl_seconds, 3600, "cache.default_ttl_seconds"),
|
|
424
|
+
biTtlSeconds: positiveInt(c.bi_ttl_seconds, 900, "cache.bi_ttl_seconds"),
|
|
425
|
+
connectTimeoutMs: positiveInt(c.connect_timeout_ms, 3000, "cache.connect_timeout_ms"),
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
393
429
|
export function replayMode(raw: unknown): ReplayMode {
|
|
394
430
|
const mode = String(raw || "verify").toLowerCase()
|
|
395
431
|
if (mode === "trust" || mode === "off") return mode
|
|
@@ -444,29 +480,82 @@ const REMOTE_BROWSER_DEFAULTS: Record<RemoteBrowserProvider, Pick<BrowserRemoteC
|
|
|
444
480
|
},
|
|
445
481
|
}
|
|
446
482
|
|
|
483
|
+
const REMOTE_BROWSER_PROVIDERS = Object.keys(REMOTE_BROWSER_DEFAULTS) as RemoteBrowserProvider[]
|
|
484
|
+
const REMOTE_BROWSER_KEYS = new Set([
|
|
485
|
+
"browser",
|
|
486
|
+
"browser_version",
|
|
487
|
+
"browserVersion",
|
|
488
|
+
"os",
|
|
489
|
+
"os_version",
|
|
490
|
+
"osVersion",
|
|
491
|
+
"platform",
|
|
492
|
+
"username_env",
|
|
493
|
+
"usernameEnv",
|
|
494
|
+
"access_key_env",
|
|
495
|
+
"accessKeyEnv",
|
|
496
|
+
"endpoint",
|
|
497
|
+
"project",
|
|
498
|
+
"build",
|
|
499
|
+
"name",
|
|
500
|
+
"local",
|
|
501
|
+
"local_testing",
|
|
502
|
+
"extra",
|
|
503
|
+
"capabilities",
|
|
504
|
+
])
|
|
505
|
+
|
|
447
506
|
function objectMap(raw: unknown, where: string): Record<string, unknown> | undefined {
|
|
448
507
|
if (raw === undefined || raw === null) return undefined
|
|
449
508
|
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${where} must be a map of names to values.`)
|
|
450
509
|
return { ...(raw as Record<string, unknown>) }
|
|
451
510
|
}
|
|
452
511
|
|
|
453
|
-
function
|
|
512
|
+
function browserProviderFiles(raw: Record<string, unknown>, base: string): BrowserProviderFiles {
|
|
513
|
+
const files = objectMap(raw.providers, "browser.providers")
|
|
514
|
+
const out: BrowserProviderFiles = {}
|
|
515
|
+
for (const provider of REMOTE_BROWSER_PROVIDERS) {
|
|
516
|
+
const file = files?.[provider]
|
|
517
|
+
if (file === undefined || file === null || file === false || String(file).trim() === "") continue
|
|
518
|
+
out[provider] = path.resolve(base, String(file))
|
|
519
|
+
}
|
|
520
|
+
return out
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function browserProviderFileSection(file: string, provider: RemoteBrowserProvider): Record<string, unknown> {
|
|
524
|
+
if (!existsSync(file)) throw new Error(`browser.providers.${provider} not found: ${file}`)
|
|
525
|
+
const doc = readYaml(file)
|
|
526
|
+
const section = Object.keys(asRecord(doc.browser)).length ? { ...asRecord(doc.browser) } : { ...doc }
|
|
527
|
+
if (!section.provider && doc.provider !== undefined) section.provider = doc.provider
|
|
528
|
+
if (!Object.keys(asRecord(section.remote)).length) {
|
|
529
|
+
const rootRemote = asRecord(doc.remote)
|
|
530
|
+
if (Object.keys(rootRemote).length) section.remote = rootRemote
|
|
531
|
+
else {
|
|
532
|
+
const directRemote = Object.fromEntries(Object.entries(section).filter(([k]) => REMOTE_BROWSER_KEYS.has(k)))
|
|
533
|
+
if (Object.keys(directRemote).length) section.remote = directRemote
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (section.provider !== undefined && browserProvider(section.provider) !== provider) {
|
|
537
|
+
throw new Error(`browser.providers.${provider} points to ${file}, but that file declares provider ${String(section.provider)}.`)
|
|
538
|
+
}
|
|
539
|
+
return section
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function browserRemoteConfig(raw: Record<string, unknown>, provider: BrowserProvider, where = "browser"): BrowserRemoteConfig | undefined {
|
|
454
543
|
if (provider === "local") return undefined
|
|
455
544
|
const remote = asRecord(raw.remote)
|
|
456
545
|
const defaults = REMOTE_BROWSER_DEFAULTS[provider]
|
|
457
546
|
const browser = String(remote.browser || "").trim()
|
|
458
|
-
if (!browser) throw new Error(
|
|
547
|
+
if (!browser) throw new Error(`${where}.remote.browser is required when browser.provider is ${provider}.`)
|
|
459
548
|
const browserVersion = String(remote.browser_version || remote.browserVersion || "latest").trim() || "latest"
|
|
460
549
|
const os = String(remote.os || "").trim() || undefined
|
|
461
550
|
const osVersion = String(remote.os_version || remote.osVersion || "").trim() || undefined
|
|
462
551
|
const platform = String(remote.platform || "").trim() || undefined
|
|
463
552
|
if (provider === "browserstack" && (!os || !osVersion)) {
|
|
464
|
-
throw new Error(
|
|
553
|
+
throw new Error(`${where}.remote.os and ${where}.remote.os_version are required when browser.provider is browserstack.`)
|
|
465
554
|
}
|
|
466
555
|
if (provider === "testmu" && !platform && !os) {
|
|
467
|
-
throw new Error(
|
|
556
|
+
throw new Error(`${where}.remote.platform is required when browser.provider is testmu, or set ${where}.remote.os (and optionally os_version).`)
|
|
468
557
|
}
|
|
469
|
-
const extra = objectMap(remote.extra ?? remote.capabilities,
|
|
558
|
+
const extra = objectMap(remote.extra ?? remote.capabilities, `${where}.remote.extra`)
|
|
470
559
|
return {
|
|
471
560
|
provider,
|
|
472
561
|
usernameEnv: String(remote.username_env || remote.usernameEnv || defaults.usernameEnv),
|
|
@@ -485,6 +574,35 @@ function browserRemoteConfig(raw: Record<string, unknown>, provider: BrowserProv
|
|
|
485
574
|
}
|
|
486
575
|
}
|
|
487
576
|
|
|
577
|
+
function browserRemoteConfigs(browser: Record<string, unknown>, provider: BrowserProvider, base: string): { remotes: BrowserRemoteConfigs; providerFiles: BrowserProviderFiles } {
|
|
578
|
+
const providerFiles = browserProviderFiles(browser, base)
|
|
579
|
+
const remotes: BrowserRemoteConfigs = {}
|
|
580
|
+
for (const remoteProvider of REMOTE_BROWSER_PROVIDERS) {
|
|
581
|
+
const fromFile = providerFiles[remoteProvider] ? browserProviderFileSection(providerFiles[remoteProvider]!, remoteProvider) : undefined
|
|
582
|
+
const inline = provider === remoteProvider ? browser : undefined
|
|
583
|
+
if (!fromFile && !inline) continue
|
|
584
|
+
const where = fromFile ? `browser.providers.${remoteProvider}` : "browser"
|
|
585
|
+
remotes[remoteProvider] = browserRemoteConfig(
|
|
586
|
+
{
|
|
587
|
+
...(fromFile || {}),
|
|
588
|
+
...(inline || {}),
|
|
589
|
+
remote: { ...asRecord(fromFile?.remote), ...asRecord(inline?.remote) },
|
|
590
|
+
},
|
|
591
|
+
remoteProvider,
|
|
592
|
+
where,
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
return { remotes, providerFiles }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function browserConfigFor(config: Pick<AppConfig, "browser">, provider = config.browser.provider): Pick<BrowserConfig, "provider" | "remote"> {
|
|
599
|
+
const chosen = provider || config.browser.provider || "local"
|
|
600
|
+
if (chosen === "local") return { provider: chosen }
|
|
601
|
+
const remote = config.browser.remotes?.[chosen] || ((config.browser.provider === chosen || !config.browser.provider) ? config.browser.remote : undefined)
|
|
602
|
+
if (remote) return { provider: chosen, remote }
|
|
603
|
+
throw new Error(`Browser provider ${chosen} is not configured. Add browser.providers.${chosen} to forgeqe.yaml, or set browser.provider: ${chosen} with browser.remote.`)
|
|
604
|
+
}
|
|
605
|
+
|
|
488
606
|
function profileFromRaw(name: string, raw: Record<string, unknown>): LlmProfile {
|
|
489
607
|
const provider = String(raw.provider || "openai") as LlmProfile["provider"]
|
|
490
608
|
return {
|
|
@@ -656,14 +774,17 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
656
774
|
: []
|
|
657
775
|
|
|
658
776
|
const sessionsDir = configPath ? path.join(projectDir(base), "sessions") : path.join(forgeqeHome(), "sessions")
|
|
777
|
+
const provider = browserProvider(browser.provider)
|
|
778
|
+
const { remotes, providerFiles } = browserRemoteConfigs(browser, provider, base)
|
|
659
779
|
const config: AppConfig = {
|
|
660
780
|
configPath,
|
|
661
781
|
mcp: extraMcpServers(yaml.mcp, base),
|
|
782
|
+
cache: cacheConfig(yaml.cache),
|
|
662
783
|
activeProfile,
|
|
663
784
|
profiles,
|
|
664
785
|
llm: profiles[activeProfile] || { ...NO_PROFILE },
|
|
665
786
|
browser: {
|
|
666
|
-
provider
|
|
787
|
+
provider,
|
|
667
788
|
headless: browser.headless !== false,
|
|
668
789
|
width: Number(viewport.width || 1280),
|
|
669
790
|
height: Number(viewport.height || 800),
|
|
@@ -672,7 +793,9 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
672
793
|
visionDetail: visionDetail(browser.vision_detail),
|
|
673
794
|
allowedHosts: hosts,
|
|
674
795
|
startUrl: browser.start_url ? String(browser.start_url) : undefined,
|
|
675
|
-
remote:
|
|
796
|
+
remote: provider === "local" ? undefined : remotes[provider],
|
|
797
|
+
remotes,
|
|
798
|
+
providerFiles,
|
|
676
799
|
overlay: browser.overlay !== false,
|
|
677
800
|
judge: browser.judge === true,
|
|
678
801
|
},
|
package/src/heal/loop.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { addUsage, costOf, meterSince, meterSnapshot, type Cost } from "../usage
|
|
|
7
7
|
import { openSession, type OpencodeSession } from "../code/runtime.ts"
|
|
8
8
|
import { runTestCommand, type TestRun } from "../code/testrun.ts"
|
|
9
9
|
import { changedFiles, ownPaths, protectPatterns, restoreRemoved, snapshot } from "../code/worktree.ts"
|
|
10
|
-
import type { AppConfig } from "../config.ts"
|
|
10
|
+
import type { AppConfig, BrowserProvider } from "../config.ts"
|
|
11
11
|
import { MCP_NAME, TOOL_TIMEOUT_MS, localToolServer } from "../mcp/server.ts"
|
|
12
12
|
import { TOOLS, environmentNotes, type ToolContext, type ToolEvent } from "../mcp/tools.ts"
|
|
13
13
|
import { runDirectory } from "../run.ts"
|
|
@@ -79,6 +79,7 @@ export type HealOptions = {
|
|
|
79
79
|
/** The repo's test command; default code.test_command, else the agent finds it. */
|
|
80
80
|
command?: string
|
|
81
81
|
outDir?: string
|
|
82
|
+
browserProvider?: BrowserProvider
|
|
82
83
|
headed?: boolean
|
|
83
84
|
maxSteps?: number
|
|
84
85
|
url?: string
|
|
@@ -149,6 +150,7 @@ export async function runHeal(opts: HealOptions): Promise<HealResult> {
|
|
|
149
150
|
config: runsConfig,
|
|
150
151
|
cwd: repo,
|
|
151
152
|
headed: opts.headed,
|
|
153
|
+
browserProvider: opts.browserProvider,
|
|
152
154
|
maxSteps: opts.maxSteps,
|
|
153
155
|
url: opts.url,
|
|
154
156
|
signal: opts.signal,
|
package/src/init.ts
CHANGED
|
@@ -64,6 +64,24 @@ export function initWorkspace(
|
|
|
64
64
|
// Prompts, skills, and product knowledge templates, to read and change: .forgeqe/ in a project, ~/.forgeqe for the user.
|
|
65
65
|
const root = home ? forgeqeHome() : projectDir(directory)
|
|
66
66
|
const shown = (p: string) => (home ? p : path.relative(directory, p) || p)
|
|
67
|
+
for (const [srcName, destName] of [
|
|
68
|
+
["browserstack.example.yaml", "browserstack.yaml"],
|
|
69
|
+
["testmu.example.yaml", "testmu.yaml"],
|
|
70
|
+
] as const) {
|
|
71
|
+
const src = path.join(ROOT, srcName)
|
|
72
|
+
const dest = path.join(root, destName)
|
|
73
|
+
if (!existsSync(src)) {
|
|
74
|
+
notes.push(`Template missing: ${src}`)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (existsSync(dest) && !opts.force) {
|
|
78
|
+
notes.push(`Kept existing ${shown(dest)}`)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
mkdirSync(path.dirname(dest), { recursive: true })
|
|
82
|
+
writeFileSync(dest, readFileSync(src, "utf8"), "utf8")
|
|
83
|
+
created.push(dest)
|
|
84
|
+
}
|
|
67
85
|
for (const { what, src, dest, keys } of [
|
|
68
86
|
{ what: "prompts", src: PROMPTS_DIR, dest: path.join(root, "prompts"), keys: entries(PROMPTS_DIR, (e) => e.endsWith(".md")) },
|
|
69
87
|
{ what: "skills", src: PREBUILT_DIR, dest: path.join(root, "skills"), keys: entries(PREBUILT_DIR, (e) => existsSync(path.join(PREBUILT_DIR, e, "SKILL.md"))) },
|
package/src/mcp/tools.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { buildHandoff } from "../code/handoff.ts"
|
|
|
15
15
|
import { latestRunDir, runCodeAgent } from "../code/index.ts"
|
|
16
16
|
import type { OpencodeSession } from "../code/runtime.ts"
|
|
17
17
|
import { runTestCommand } from "../code/testrun.ts"
|
|
18
|
-
import type
|
|
18
|
+
import { browserProvider, type AppConfig } from "../config.ts"
|
|
19
19
|
import { clearMemory, memoryFilesFor, memoryFlows, memoryOverview, memoryShow } from "../memory.ts"
|
|
20
20
|
import { executeRun, runDirectory } from "../run.ts"
|
|
21
21
|
import { counts } from "../suite/report.ts"
|
|
@@ -123,6 +123,7 @@ export type ToolContext = {
|
|
|
123
123
|
/** The repository or folder the agent works in. */
|
|
124
124
|
cwd: string
|
|
125
125
|
headed?: boolean
|
|
126
|
+
browserProvider?: AppConfig["browser"]["provider"]
|
|
126
127
|
maxSteps?: number
|
|
127
128
|
replay?: ReplayMode
|
|
128
129
|
memory?: boolean
|
|
@@ -256,7 +257,7 @@ export const TOOLS: McpTool[] = [
|
|
|
256
257
|
{
|
|
257
258
|
name: "browser_run",
|
|
258
259
|
description:
|
|
259
|
-
"Carry out a goal in a real browser with ForgeQE's browser agent and verify the outcome. The agent picks the site from the goal unless start_url is given. Returns pass/fail, the agent's summary, the steps, and the run folder (run.json, workflow.yaml with ranked locators, trace.md, steps and screenshots, report.html). Takes minutes.",
|
|
260
|
+
"Carry out a goal in a real browser with ForgeQE's browser agent and verify the outcome. The browser comes from forgeqe.yaml: local Chrome or Chromium by default, or BrowserStack or TestMu AI/LambdaTest when browser.provider selects one. The agent picks the site from the goal unless start_url is given. Returns pass/fail, the agent's summary, the steps, and the run folder (run.json, workflow.yaml with ranked locators, trace.md, steps and screenshots, report.html). Takes minutes.",
|
|
260
261
|
inputSchema: {
|
|
261
262
|
type: "object",
|
|
262
263
|
properties: {
|
|
@@ -266,6 +267,12 @@ export const TOOLS: McpTool[] = [
|
|
|
266
267
|
"The final instructions for the browser agent: what to do and what to check, in plain language, with every value it needs. Only the decided task; never your own questions, alternatives, or reasoning about it.",
|
|
267
268
|
},
|
|
268
269
|
start_url: { type: "string", description: "Page to open first, only when the user named one" },
|
|
270
|
+
browser_provider: {
|
|
271
|
+
type: "string",
|
|
272
|
+
enum: ["local", "browserstack", "testmu"],
|
|
273
|
+
description:
|
|
274
|
+
"Override the browser provider for this run when the user asked for one specifically. local uses the machine's own browser; browserstack and testmu use their configured YAML or browser.remote settings.",
|
|
275
|
+
},
|
|
269
276
|
resume_from: {
|
|
270
277
|
type: "string",
|
|
271
278
|
description:
|
|
@@ -287,6 +294,7 @@ export const TOOLS: McpTool[] = [
|
|
|
287
294
|
const from = args.resume_from ? String(args.resume_from) : ""
|
|
288
295
|
const resumeFrom = from ? (existsSync(path.resolve(ctx.cwd, from)) ? path.resolve(ctx.cwd, from) : from) : undefined
|
|
289
296
|
const url = args.start_url ? String(args.start_url) : ctx.url
|
|
297
|
+
const selectedBrowser = args.browser_provider ? browserProvider(args.browser_provider) : ctx.browserProvider
|
|
290
298
|
const runDir = runDirectory(ctx.config.export.outDir)
|
|
291
299
|
const emit = ctx.emit || (() => {})
|
|
292
300
|
const t0 = Date.now()
|
|
@@ -295,6 +303,7 @@ export const TOOLS: McpTool[] = [
|
|
|
295
303
|
const { result, files } = await (ctx.execute || executeRun)({
|
|
296
304
|
config: ctx.config,
|
|
297
305
|
goal,
|
|
306
|
+
browserProvider: selectedBrowser,
|
|
298
307
|
url,
|
|
299
308
|
headed: ctx.headed,
|
|
300
309
|
maxSteps: ctx.maxSteps,
|
|
@@ -410,13 +419,19 @@ export const TOOLS: McpTool[] = [
|
|
|
410
419
|
{
|
|
411
420
|
name: "run_test_cases",
|
|
412
421
|
description:
|
|
413
|
-
"Run test cases in real browsers: each case is one browser goal whose steps are done in order and whose expected results are checked, with a verdict for every step. Writes report.html (steps, screenshots, timings) and junit.xml, posts results back to Azure DevOps, Jira, Xray, TestRail, Zephyr Scale, or qTest unless write_back is false, and files or updates bugs for failures when bugs are set up. In a session that writes tests, each passed case is handed to ForgeQE's coding agent as soon as its browser run ends, and its test is written and run before the next case starts. Returns each case's result, step verdicts, run folder, and test (file, command, status, or why none was written). Takes minutes.",
|
|
422
|
+
"Run test cases in real browsers: each case is one browser goal whose steps are done in order and whose expected results are checked, with a verdict for every step. The browser comes from forgeqe.yaml: local Chrome or Chromium by default, or BrowserStack or TestMu AI/LambdaTest when browser.provider selects one. Writes report.html (steps, screenshots, timings) and junit.xml, posts results back to Azure DevOps, Jira, Xray, TestRail, Zephyr Scale, or qTest unless write_back is false, and files or updates bugs for failures when bugs are set up. In a session that writes tests, each passed case is handed to ForgeQE's coding agent as soon as its browser run ends, and its test is written and run before the next case starts. Returns each case's result, step verdicts, run folder, and test (file, command, status, or why none was written). Takes minutes.",
|
|
414
423
|
inputSchema: {
|
|
415
424
|
type: "object",
|
|
416
425
|
properties: {
|
|
417
426
|
sources: SOURCES_SCHEMA,
|
|
418
427
|
parallel: { type: "integer", minimum: 1, maximum: 8, description: "Browsers at once (default from config)" },
|
|
419
428
|
write_back: { type: "boolean", description: "Post results to the source system (default from config)" },
|
|
429
|
+
browser_provider: {
|
|
430
|
+
type: "string",
|
|
431
|
+
enum: ["local", "browserstack", "testmu"],
|
|
432
|
+
description:
|
|
433
|
+
"Override the browser provider for every case in this suite when the user asked for one specifically.",
|
|
434
|
+
},
|
|
420
435
|
skills: SKILLS_SCHEMA,
|
|
421
436
|
},
|
|
422
437
|
required: ["sources"],
|
|
@@ -429,6 +444,7 @@ export const TOOLS: McpTool[] = [
|
|
|
429
444
|
if (!cases.length) return { ran: false, reason: "No case can run as written (suite.case_check: block).", check: checkResult(checks) }
|
|
430
445
|
if (!cases.length) throw new Error("No test cases found.")
|
|
431
446
|
const parallel = Math.min(Number(args.parallel) || ctx.config.suite.parallel, cases.length)
|
|
447
|
+
const selectedBrowser = args.browser_provider ? browserProvider(args.browser_provider) : ctx.browserProvider
|
|
432
448
|
const emit = ctx.emit || (() => {})
|
|
433
449
|
emit({ type: "suite-start", cases, parallel })
|
|
434
450
|
// In a session that writes tests, each passed case goes to the coding agent right after its own browser run,
|
|
@@ -455,6 +471,7 @@ export const TOOLS: McpTool[] = [
|
|
|
455
471
|
config: ctx.writeTests ? { ...ctx.config, suite: { ...ctx.config.suite, code: true } } : ctx.config,
|
|
456
472
|
cases,
|
|
457
473
|
...(ctx.writeTests ? { repo: ctx.cwd, coder } : {}),
|
|
474
|
+
browserProvider: selectedBrowser,
|
|
458
475
|
parallel,
|
|
459
476
|
writeBack: typeof args.write_back === "boolean" ? args.write_back : undefined,
|
|
460
477
|
headed: ctx.headed,
|
|
@@ -765,7 +782,7 @@ function knowledgeNotes(cfg: AppConfig): string {
|
|
|
765
782
|
: `No product knowledge yet: the team can describe the product, roles, rules, and test data in ${cfg.knowledge.dir} (forgeqe init adds templates).`
|
|
766
783
|
}
|
|
767
784
|
|
|
768
|
-
export function environmentNotes(ctx: Pick<ToolContext, "config" | "cwd" | "url">): string {
|
|
785
|
+
export function environmentNotes(ctx: Pick<ToolContext, "config" | "cwd" | "url" | "browserProvider">): string {
|
|
769
786
|
const { config: cfg } = ctx
|
|
770
787
|
const s = cfg.sources
|
|
771
788
|
const systems = [
|
|
@@ -802,13 +819,17 @@ export function environmentNotes(ctx: Pick<ToolContext, "config" | "cwd" | "url"
|
|
|
802
819
|
const monitor = m.checks.length || channels.length
|
|
803
820
|
? `Monitoring (monitor: in forgeqe.yaml): ${m.checks.length ? `checks ${m.checks.join(" ")}` : "no checks yet"}, every ${Math.round(m.everyMs / 60_000)} min, alerts to ${channels.length ? channels.join(" and ") : "nobody yet"}.`
|
|
804
821
|
: ""
|
|
822
|
+
const browser = cfg.browser || {}
|
|
805
823
|
const browserSkills = skillsFor(loadSkills(cfg.skills, ctx.cwd), "browser")
|
|
824
|
+
const browserProvider = browser.provider || "local"
|
|
825
|
+
const remoteProviders = (["browserstack", "testmu"] as const).filter((name) => browser.remotes?.[name])
|
|
806
826
|
const skills = browserSkills.length
|
|
807
827
|
? [`Browser skills (pass the ones a goal needs as skills to browser_run or run_test_cases):`, ...catalogLines(browserSkills)].join("\n")
|
|
808
828
|
: ""
|
|
809
829
|
return [
|
|
810
830
|
`Working directory: ${ctx.cwd}`,
|
|
811
831
|
`Browser runs are saved under ${cfg.export.outDir}; page memory is in ${cfg.memory.dir}.`,
|
|
832
|
+
`Browser provider default: ${browserProvider}.${remoteProviders.length ? ` Also configured: ${remoteProviders.join(", ")}. Pass browser_provider to browser_run or run_test_cases when the user asks for one of them.` : ""}`,
|
|
812
833
|
`Test case systems configured: ${systems.length ? systems.join("; ") : "none (files and text still work)"}.`,
|
|
813
834
|
s.confluence ? `Confluence: ${s.confluence.url}${s.confluence.space ? `, space ${s.confluence.space}` : ""} (read_confluence).` : "",
|
|
814
835
|
`Sprints (list_sprints, find_tests sprint): ${[s.ado ? "Azure DevOps iterations" : "", s.jira ? "Jira scrum boards" : ""].filter(Boolean).join(" and ") || "none configured; ask the user for dates"}.`,
|
|
@@ -831,6 +852,7 @@ export function environmentNotes(ctx: Pick<ToolContext, "config" | "cwd" | "url"
|
|
|
831
852
|
`Browser model profile: ${cfg.activeProfile} (${cfg.llm.provider}/${cfg.llm.deployment || cfg.llm.model}).`,
|
|
832
853
|
keys.length ? `Set in ForgeQE's environment (names only, usable with set_ci_secret): ${keys.join(", ")}.` : "",
|
|
833
854
|
ctx.url ? `The user pinned this start URL for browser runs: ${ctx.url}` : "",
|
|
855
|
+
ctx.browserProvider ? `The user pinned this browser provider for runs in this session: ${ctx.browserProvider}.` : "",
|
|
834
856
|
skills,
|
|
835
857
|
domainNotes(cfg),
|
|
836
858
|
knowledgeNotes(cfg),
|
package/src/monitor/run.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path"
|
|
|
3
3
|
|
|
4
4
|
import type { Card } from "../card.ts"
|
|
5
5
|
import { loadCases, type TestCase } from "../cases/index.ts"
|
|
6
|
-
import type { AppConfig } from "../config.ts"
|
|
6
|
+
import type { AppConfig, BrowserProvider } from "../config.ts"
|
|
7
7
|
import { counts, suiteUsage } from "../suite/report.ts"
|
|
8
8
|
import { runSuite, type SuiteEvent, type SuiteOptions, type SuiteResult } from "../suite/run.ts"
|
|
9
9
|
import { formatDuration, formatTokens } from "../tui/theme.ts"
|
|
@@ -28,6 +28,7 @@ export type MonitorOptions = {
|
|
|
28
28
|
config: AppConfig
|
|
29
29
|
/** Test case sources; default monitor.checks. */
|
|
30
30
|
checks: string[]
|
|
31
|
+
browserProvider?: BrowserProvider
|
|
31
32
|
headed?: boolean
|
|
32
33
|
url?: string
|
|
33
34
|
/** Browser skills for every check (empty: none); unset: the engine picks once per check for the monitor's lifetime. */
|
|
@@ -67,6 +68,7 @@ export async function monitorRound(opts: MonitorOptions): Promise<RoundResult> {
|
|
|
67
68
|
suite = await runSuite({
|
|
68
69
|
config: { ...config, suite: { ...config.suite, outDir: m.outDir, code: false } },
|
|
69
70
|
cases,
|
|
71
|
+
browserProvider: opts.browserProvider,
|
|
70
72
|
parallel: Math.min(m.parallel, cases.length),
|
|
71
73
|
writeBack: m.writeBack,
|
|
72
74
|
replay: m.replay,
|
package/src/run.ts
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path"
|
|
|
5
5
|
import { assertGoalUrlsAllowed, hostAllowed, normalizeHttpUrl } from "./guard.ts"
|
|
6
6
|
import { missingKey } from "./llm.ts"
|
|
7
7
|
import type { AgentEvent, ReplayMode, RunResult } from "./types.ts"
|
|
8
|
-
import type
|
|
8
|
+
import { browserConfigFor, type AppConfig, type BrowserProvider } from "./config.ts"
|
|
9
9
|
import { runEngine } from "./engine/bridge.ts"
|
|
10
10
|
import { MOCK_ENGINE } from "./engine/mock.ts"
|
|
11
11
|
import { ensureEngine } from "./engine/setup.ts"
|
|
@@ -23,6 +23,7 @@ import { knowledgeBlock, readKnowledge, relevantKnowledge } from "./knowledge.ts
|
|
|
23
23
|
export type ExecuteOptions = {
|
|
24
24
|
config: AppConfig
|
|
25
25
|
goal: string
|
|
26
|
+
browserProvider?: BrowserProvider
|
|
26
27
|
url?: string
|
|
27
28
|
headed?: boolean
|
|
28
29
|
maxSteps?: number
|
|
@@ -148,6 +149,7 @@ export async function executeRun(opts: ExecuteOptions): Promise<{ result: RunRes
|
|
|
148
149
|
|
|
149
150
|
const dir = opts.outDir || runDirectory(opts.config.export.outDir)
|
|
150
151
|
const maxSteps = opts.maxSteps || opts.config.browser.maxSteps
|
|
152
|
+
const browser = browserConfigFor(opts.config, opts.browserProvider)
|
|
151
153
|
const t0 = Date.now()
|
|
152
154
|
const withReport = (out: { result: RunResult; files: string[] }) => {
|
|
153
155
|
out.result.cost = costOf(out.result.usage, opts.config.llm.price)
|
|
@@ -216,7 +218,7 @@ export async function executeRun(opts: ExecuteOptions): Promise<{ result: RunRes
|
|
|
216
218
|
headed: Boolean(opts.headed) || !opts.config.browser.headless,
|
|
217
219
|
maxSteps,
|
|
218
220
|
allowedHosts: allowed,
|
|
219
|
-
browser
|
|
221
|
+
browser,
|
|
220
222
|
extendSystemMessage: extend || undefined,
|
|
221
223
|
memory,
|
|
222
224
|
env,
|