opencode-pair-autonomy 1.0.0
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 +90 -0
- package/bin/opencode-pair-autonomy.js +20 -0
- package/dist/__tests__/comment-guard.test.d.ts +1 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/learning.test.d.ts +1 -0
- package/dist/__tests__/plan-mode.test.d.ts +1 -0
- package/dist/agents.d.ts +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +15351 -0
- package/dist/commands.d.ts +2 -0
- package/dist/config.d.ts +3 -0
- package/dist/hooks/comment-guard.d.ts +15 -0
- package/dist/hooks/file-edited.d.ts +7 -0
- package/dist/hooks/index.d.ts +46 -0
- package/dist/hooks/post-tool-use.d.ts +5 -0
- package/dist/hooks/pre-compact.d.ts +4 -0
- package/dist/hooks/pre-tool-use.d.ts +5 -0
- package/dist/hooks/prompt-refiner.d.ts +38 -0
- package/dist/hooks/runtime.d.ts +91 -0
- package/dist/hooks/sdk.d.ts +6 -0
- package/dist/hooks/session-end.d.ts +4 -0
- package/dist/hooks/session-start.d.ts +19 -0
- package/dist/hooks/stop.d.ts +5 -0
- package/dist/i18n/index.d.ts +15 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +17823 -0
- package/dist/installer.d.ts +12 -0
- package/dist/learning/analyzer.d.ts +15 -0
- package/dist/learning/store.d.ts +4 -0
- package/dist/learning/types.d.ts +32 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/project-facts.d.ts +8 -0
- package/dist/prompts/coordinator.d.ts +2 -0
- package/dist/prompts/shared.d.ts +5 -0
- package/dist/prompts/workers.d.ts +8 -0
- package/dist/types.d.ts +81 -0
- package/dist/utils.d.ts +6 -0
- package/examples/opencode-pair-autonomy.jsonc +35 -0
- package/examples/opencode.jsonc +17 -0
- package/package.json +103 -0
- package/vendor/mcp/pg-mcp/README.md +91 -0
- package/vendor/mcp/pg-mcp/config.example.json +26 -0
- package/vendor/mcp/pg-mcp/config.json +15 -0
- package/vendor/mcp/pg-mcp/package-lock.json +1288 -0
- package/vendor/mcp/pg-mcp/package.json +18 -0
- package/vendor/mcp/pg-mcp/src/config.js +71 -0
- package/vendor/mcp/pg-mcp/src/db.js +85 -0
- package/vendor/mcp/pg-mcp/src/index.js +203 -0
- package/vendor/mcp/pg-mcp/src/sqlGuard.js +75 -0
- package/vendor/mcp/pg-mcp/src/tools.js +89 -0
- package/vendor/mcp/ssh-mcp/README.md +46 -0
- package/vendor/mcp/ssh-mcp/config.example.json +23 -0
- package/vendor/mcp/ssh-mcp/config.json +6 -0
- package/vendor/mcp/ssh-mcp/package-lock.json +1142 -0
- package/vendor/mcp/ssh-mcp/package.json +18 -0
- package/vendor/mcp/ssh-mcp/src/config.js +140 -0
- package/vendor/mcp/ssh-mcp/src/index.js +130 -0
- package/vendor/mcp/ssh-mcp/src/ssh.js +163 -0
- package/vendor/mcp/sudo-mcp/README.md +51 -0
- package/vendor/mcp/sudo-mcp/config.example.json +28 -0
- package/vendor/mcp/sudo-mcp/config.json +28 -0
- package/vendor/mcp/sudo-mcp/package-lock.json +1145 -0
- package/vendor/mcp/sudo-mcp/package.json +18 -0
- package/vendor/mcp/sudo-mcp/src/config.js +57 -0
- package/vendor/mcp/sudo-mcp/src/index.js +267 -0
- package/vendor/mcp/sudo-mcp/src/runner.js +168 -0
- package/vendor/mcp/web-agent-mcp/package-lock.json +2886 -0
- package/vendor/mcp/web-agent-mcp/package.json +28 -0
- package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/adapter.ts +335 -0
- package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/auth-heuristics.ts +324 -0
- package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/launcher.ts +1340 -0
- package/vendor/mcp/web-agent-mcp/src/config/env.ts +107 -0
- package/vendor/mcp/web-agent-mcp/src/core/action-flow.ts +82 -0
- package/vendor/mcp/web-agent-mcp/src/core/artifact-store.ts +109 -0
- package/vendor/mcp/web-agent-mcp/src/core/errors.ts +108 -0
- package/vendor/mcp/web-agent-mcp/src/core/observation-flow.ts +38 -0
- package/vendor/mcp/web-agent-mcp/src/core/policy-engine.ts +113 -0
- package/vendor/mcp/web-agent-mcp/src/core/retry-policy.ts +42 -0
- package/vendor/mcp/web-agent-mcp/src/core/session-manager.ts +670 -0
- package/vendor/mcp/web-agent-mcp/src/core/session-restart-policy.ts +34 -0
- package/vendor/mcp/web-agent-mcp/src/core/task-history.ts +97 -0
- package/vendor/mcp/web-agent-mcp/src/index.ts +3 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/act.ts +167 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/common.ts +56 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/observe.ts +214 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/page.ts +21 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/policy.ts +42 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/runtime.ts +21 -0
- package/vendor/mcp/web-agent-mcp/src/schemas/session.ts +63 -0
- package/vendor/mcp/web-agent-mcp/src/server.ts +75 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/click.ts +68 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/drag.ts +57 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/enter-code.ts +78 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/fill.ts +65 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/pinch.ts +58 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/press.ts +67 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/shared.ts +73 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/swipe.ts +59 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/wait-for.ts +56 -0
- package/vendor/mcp/web-agent-mcp/src/tools/act/wheel.ts +59 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/a11y.ts +60 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/auth-state.ts +92 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/boxes.ts +66 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/console.ts +67 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/dom.ts +60 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/network.ts +67 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/page-state.ts +93 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/screenshot.ts +73 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/text.ts +70 -0
- package/vendor/mcp/web-agent-mcp/src/tools/observe/wait-for-network.ts +70 -0
- package/vendor/mcp/web-agent-mcp/src/tools/page/navigate.ts +59 -0
- package/vendor/mcp/web-agent-mcp/src/tools/policy/recommend-observation.ts +40 -0
- package/vendor/mcp/web-agent-mcp/src/tools/register-tools.ts +55 -0
- package/vendor/mcp/web-agent-mcp/src/tools/runtime/evaluate-js.ts +83 -0
- package/vendor/mcp/web-agent-mcp/src/tools/session/close.ts +41 -0
- package/vendor/mcp/web-agent-mcp/src/tools/session/create.ts +86 -0
- package/vendor/mcp/web-agent-mcp/src/tools/session/restart.ts +72 -0
- package/vendor/mcp/web-agent-mcp/src/utils/fs.ts +28 -0
- package/vendor/mcp/web-agent-mcp/src/utils/ids.ts +9 -0
- package/vendor/mcp/web-agent-mcp/src/utils/time.ts +7 -0
- package/vendor/mcp/web-agent-mcp/tsconfig.json +22 -0
- package/vendor/skills/editorial-technical-ui/SKILL.md +84 -0
- package/vendor/skills/figma-console/SKILL.md +839 -0
- package/vendor/skills/go-fiber-postgres/SKILL.md +31 -0
- package/vendor/skills/opencode-plugin-dev/SKILL.md +31 -0
- package/vendor/skills/rust-media-desktop/SKILL.md +30 -0
- package/vendor/skills/vue-vite-ui/SKILL.md +31 -0
- package/vendor/skills/web-agent-browser/SKILL.md +140 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const envSchema = z.object({
|
|
5
|
+
WEB_AGENT_SERVER_NAME: z.string().default("web-agent-mcp"),
|
|
6
|
+
WEB_AGENT_SERVER_VERSION: z.string().default("0.1.0"),
|
|
7
|
+
WEB_AGENT_DATA_DIR: z.string().default(path.resolve(process.cwd(), ".data")),
|
|
8
|
+
WEB_AGENT_HEADLESS: z
|
|
9
|
+
.enum(["true", "false"])
|
|
10
|
+
.optional()
|
|
11
|
+
.transform((value) => value === "true"),
|
|
12
|
+
WEB_AGENT_DEFAULT_LOCALE: z.string().default("en-US"),
|
|
13
|
+
WEB_AGENT_DEFAULT_TIMEZONE_ID: z.string().optional(),
|
|
14
|
+
WEB_AGENT_CHROME_USER_DATA_DIR: z.string().optional(),
|
|
15
|
+
WEB_AGENT_CHROME_PROFILE_DIRECTORY: z.string().optional(),
|
|
16
|
+
WEB_AGENT_DEFAULT_HUMANIZE: z
|
|
17
|
+
.enum(["true", "false"])
|
|
18
|
+
.optional()
|
|
19
|
+
.transform((value) => value === "true"),
|
|
20
|
+
WEB_AGENT_DEFAULT_VIEWPORT_WIDTH: z.coerce
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.positive()
|
|
24
|
+
.default(1440),
|
|
25
|
+
WEB_AGENT_DEFAULT_VIEWPORT_HEIGHT: z.coerce
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.positive()
|
|
29
|
+
.default(960),
|
|
30
|
+
WEB_AGENT_DEFAULT_LAUNCH_ARGS: z.string().optional(),
|
|
31
|
+
WEB_AGENT_SESSION_MAX_CONSECUTIVE_ERRORS: z.coerce
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.positive()
|
|
35
|
+
.default(3),
|
|
36
|
+
WEB_AGENT_SESSION_RESTART_COOLDOWN_MS: z.coerce
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.positive()
|
|
40
|
+
.default(30000),
|
|
41
|
+
WEB_AGENT_HISTORY_DIR: z.string().optional(),
|
|
42
|
+
WEB_AGENT_ARTIFACTS_DIR: z.string().optional(),
|
|
43
|
+
WEB_AGENT_PROFILES_DIR: z.string().optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type WebAgentEnv = {
|
|
47
|
+
serverName: string;
|
|
48
|
+
serverVersion: string;
|
|
49
|
+
dataDir: string;
|
|
50
|
+
historyDir: string;
|
|
51
|
+
artifactsDir: string;
|
|
52
|
+
profilesDir: string;
|
|
53
|
+
headless: boolean;
|
|
54
|
+
defaultLocale: string;
|
|
55
|
+
defaultTimezoneId?: string;
|
|
56
|
+
chromeUserDataDir?: string;
|
|
57
|
+
chromeProfileDirectory?: string;
|
|
58
|
+
defaultHumanize: boolean;
|
|
59
|
+
defaultLaunchArgs: string[];
|
|
60
|
+
defaultViewport: {
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
};
|
|
64
|
+
sessionMaxConsecutiveErrors: number;
|
|
65
|
+
sessionRestartCooldownMs: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function loadEnv(source: NodeJS.ProcessEnv = process.env): WebAgentEnv {
|
|
69
|
+
const parsed = envSchema.parse(source);
|
|
70
|
+
const historyDir =
|
|
71
|
+
parsed.WEB_AGENT_HISTORY_DIR ??
|
|
72
|
+
path.join(parsed.WEB_AGENT_DATA_DIR, "history");
|
|
73
|
+
const artifactsDir =
|
|
74
|
+
parsed.WEB_AGENT_ARTIFACTS_DIR ??
|
|
75
|
+
path.join(parsed.WEB_AGENT_DATA_DIR, "artifacts");
|
|
76
|
+
const profilesDir =
|
|
77
|
+
parsed.WEB_AGENT_PROFILES_DIR ??
|
|
78
|
+
path.join(parsed.WEB_AGENT_DATA_DIR, "profiles");
|
|
79
|
+
const defaultLaunchArgs = parsed.WEB_AGENT_DEFAULT_LAUNCH_ARGS
|
|
80
|
+
? parsed.WEB_AGENT_DEFAULT_LAUNCH_ARGS.split(",")
|
|
81
|
+
.map((value) => value.trim())
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
: [];
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
serverName: parsed.WEB_AGENT_SERVER_NAME,
|
|
87
|
+
serverVersion: parsed.WEB_AGENT_SERVER_VERSION,
|
|
88
|
+
dataDir: parsed.WEB_AGENT_DATA_DIR,
|
|
89
|
+
historyDir,
|
|
90
|
+
artifactsDir,
|
|
91
|
+
profilesDir,
|
|
92
|
+
headless: parsed.WEB_AGENT_HEADLESS,
|
|
93
|
+
defaultLocale: parsed.WEB_AGENT_DEFAULT_LOCALE,
|
|
94
|
+
defaultTimezoneId: parsed.WEB_AGENT_DEFAULT_TIMEZONE_ID,
|
|
95
|
+
chromeUserDataDir: parsed.WEB_AGENT_CHROME_USER_DATA_DIR,
|
|
96
|
+
chromeProfileDirectory: parsed.WEB_AGENT_CHROME_PROFILE_DIRECTORY,
|
|
97
|
+
defaultHumanize: parsed.WEB_AGENT_DEFAULT_HUMANIZE,
|
|
98
|
+
defaultLaunchArgs,
|
|
99
|
+
defaultViewport: {
|
|
100
|
+
width: parsed.WEB_AGENT_DEFAULT_VIEWPORT_WIDTH,
|
|
101
|
+
height: parsed.WEB_AGENT_DEFAULT_VIEWPORT_HEIGHT,
|
|
102
|
+
},
|
|
103
|
+
sessionMaxConsecutiveErrors:
|
|
104
|
+
parsed.WEB_AGENT_SESSION_MAX_CONSECUTIVE_ERRORS,
|
|
105
|
+
sessionRestartCooldownMs: parsed.WEB_AGENT_SESSION_RESTART_COOLDOWN_MS,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { asWebAgentError } from "./errors.js";
|
|
2
|
+
import { buildFollowUpByGoal, toPolicyEnvelope } from "./observation-flow.js";
|
|
3
|
+
import { recommendObservation } from "./policy-engine.js";
|
|
4
|
+
import { getRetryPolicy } from "./retry-policy.js";
|
|
5
|
+
import { shouldRecommendSessionRestart } from "./session-restart-policy.js";
|
|
6
|
+
|
|
7
|
+
export function buildActionSuccessPayload(input: {
|
|
8
|
+
actionId: string;
|
|
9
|
+
appliedMode: "semantic" | "physical";
|
|
10
|
+
verificationHint?: string;
|
|
11
|
+
targetSelectorKnown: boolean;
|
|
12
|
+
}) {
|
|
13
|
+
return {
|
|
14
|
+
action_id: input.actionId,
|
|
15
|
+
applied_mode: input.appliedMode,
|
|
16
|
+
verification_hint: input.verificationHint,
|
|
17
|
+
follow_up_by_goal: buildFollowUpByGoal({
|
|
18
|
+
pageStateKnown: true,
|
|
19
|
+
targetSelectorKnown: input.targetSelectorKnown,
|
|
20
|
+
recentFailure: "none",
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildActionFailurePayload(input: {
|
|
26
|
+
error: unknown;
|
|
27
|
+
actionId?: string;
|
|
28
|
+
appliedMode: "semantic" | "physical";
|
|
29
|
+
targetSelectorKnown: boolean;
|
|
30
|
+
sessionHealth?: {
|
|
31
|
+
consecutiveErrors: number;
|
|
32
|
+
maxConsecutiveErrors: number;
|
|
33
|
+
cooldownMs: number;
|
|
34
|
+
lastRestartAt?: string;
|
|
35
|
+
now: string;
|
|
36
|
+
};
|
|
37
|
+
}) {
|
|
38
|
+
const mapped = asWebAgentError(input.error);
|
|
39
|
+
const recentFailure =
|
|
40
|
+
mapped.code === "STATE_ELEMENT_NOT_FOUND"
|
|
41
|
+
? "semantic_not_found"
|
|
42
|
+
: mapped.code === "STATE_TARGET_NOT_INTERACTABLE"
|
|
43
|
+
? "semantic_failed"
|
|
44
|
+
: mapped.type === "BROWSER"
|
|
45
|
+
? "runtime_error"
|
|
46
|
+
: "layout_uncertain";
|
|
47
|
+
|
|
48
|
+
const fallback = toPolicyEnvelope(
|
|
49
|
+
recommendObservation({
|
|
50
|
+
goal: input.appliedMode === "physical" ? "visual_verify" : "interact",
|
|
51
|
+
page_state_known: true,
|
|
52
|
+
target_selector_known: input.targetSelectorKnown,
|
|
53
|
+
needs_precise_coordinates: input.appliedMode === "physical",
|
|
54
|
+
needs_full_page_context: false,
|
|
55
|
+
recent_failure: recentFailure,
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
const retryPolicy = getRetryPolicy(mapped);
|
|
59
|
+
const sessionRestart = input.sessionHealth
|
|
60
|
+
? shouldRecommendSessionRestart({
|
|
61
|
+
consecutiveErrors: input.sessionHealth.consecutiveErrors,
|
|
62
|
+
maxConsecutiveErrors: input.sessionHealth.maxConsecutiveErrors,
|
|
63
|
+
cooldownMs: input.sessionHealth.cooldownMs,
|
|
64
|
+
lastRestartAt: input.sessionHealth.lastRestartAt,
|
|
65
|
+
now: input.sessionHealth.now,
|
|
66
|
+
browserError: mapped.type === "BROWSER",
|
|
67
|
+
})
|
|
68
|
+
: { recommended: false as const };
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
action_id: input.actionId,
|
|
72
|
+
applied_mode: input.appliedMode,
|
|
73
|
+
retry_hint: retryPolicy.retryHint,
|
|
74
|
+
fallback_observation: fallback,
|
|
75
|
+
follow_up_by_goal: buildFollowUpByGoal({
|
|
76
|
+
pageStateKnown: true,
|
|
77
|
+
targetSelectorKnown: input.targetSelectorKnown,
|
|
78
|
+
recentFailure,
|
|
79
|
+
}),
|
|
80
|
+
session_restart: sessionRestart,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { WebAgentEnv } from "../config/env.js";
|
|
4
|
+
import {
|
|
5
|
+
appendJsonLine,
|
|
6
|
+
ensureDir,
|
|
7
|
+
readJsonFile,
|
|
8
|
+
writeJsonFile,
|
|
9
|
+
} from "../utils/fs.js";
|
|
10
|
+
import { createId } from "../utils/ids.js";
|
|
11
|
+
import { nowIso } from "../utils/time.js";
|
|
12
|
+
|
|
13
|
+
export type ArtifactKind =
|
|
14
|
+
| "a11y"
|
|
15
|
+
| "dom"
|
|
16
|
+
| "text"
|
|
17
|
+
| "markdown"
|
|
18
|
+
| "boxes"
|
|
19
|
+
| "screenshot"
|
|
20
|
+
| "console"
|
|
21
|
+
| "network"
|
|
22
|
+
| "eval";
|
|
23
|
+
|
|
24
|
+
export type ArtifactRecord = {
|
|
25
|
+
artifact_id: string;
|
|
26
|
+
session_id?: string;
|
|
27
|
+
page_id?: string;
|
|
28
|
+
task_id?: string;
|
|
29
|
+
action_id?: string;
|
|
30
|
+
kind: ArtifactKind;
|
|
31
|
+
created_at: string;
|
|
32
|
+
url?: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
parent_artifact_id?: string;
|
|
35
|
+
storage_path?: string;
|
|
36
|
+
bytes?: number;
|
|
37
|
+
meta?: Record<string, unknown>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export class ArtifactStore {
|
|
41
|
+
private readonly artifactPath: string;
|
|
42
|
+
private readonly indexPath: string;
|
|
43
|
+
private readonly rootDir: string;
|
|
44
|
+
|
|
45
|
+
constructor(env: WebAgentEnv) {
|
|
46
|
+
this.rootDir = env.artifactsDir;
|
|
47
|
+
this.artifactPath = path.join(env.historyDir, "artifacts.jsonl");
|
|
48
|
+
this.indexPath = path.join(env.artifactsDir, "index.json");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async writeText(
|
|
52
|
+
kind: ArtifactKind,
|
|
53
|
+
text: string,
|
|
54
|
+
refs: Omit<
|
|
55
|
+
ArtifactRecord,
|
|
56
|
+
"artifact_id" | "created_at" | "kind" | "storage_path" | "bytes"
|
|
57
|
+
> = {},
|
|
58
|
+
) {
|
|
59
|
+
const artifactId = createId("artifact");
|
|
60
|
+
const filePath = path.join(this.rootDir, `${artifactId}.txt`);
|
|
61
|
+
await ensureDir(this.rootDir);
|
|
62
|
+
await fs.writeFile(filePath, text, "utf8");
|
|
63
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
64
|
+
const record = await this.saveMetadata({
|
|
65
|
+
artifact_id: artifactId,
|
|
66
|
+
kind,
|
|
67
|
+
created_at: nowIso(),
|
|
68
|
+
storage_path: filePath,
|
|
69
|
+
bytes,
|
|
70
|
+
...refs,
|
|
71
|
+
});
|
|
72
|
+
return record;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async writeBinary(
|
|
76
|
+
kind: ArtifactKind,
|
|
77
|
+
buffer: Buffer,
|
|
78
|
+
extension: string,
|
|
79
|
+
refs: Omit<
|
|
80
|
+
ArtifactRecord,
|
|
81
|
+
"artifact_id" | "created_at" | "kind" | "storage_path" | "bytes"
|
|
82
|
+
> = {},
|
|
83
|
+
) {
|
|
84
|
+
const artifactId = createId("artifact");
|
|
85
|
+
const filePath = path.join(this.rootDir, `${artifactId}.${extension}`);
|
|
86
|
+
await ensureDir(this.rootDir);
|
|
87
|
+
await fs.writeFile(filePath, buffer);
|
|
88
|
+
const record = await this.saveMetadata({
|
|
89
|
+
artifact_id: artifactId,
|
|
90
|
+
kind,
|
|
91
|
+
created_at: nowIso(),
|
|
92
|
+
storage_path: filePath,
|
|
93
|
+
bytes: buffer.byteLength,
|
|
94
|
+
...refs,
|
|
95
|
+
});
|
|
96
|
+
return record;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async saveMetadata(record: ArtifactRecord) {
|
|
100
|
+
await appendJsonLine(this.artifactPath, record);
|
|
101
|
+
const index = await readJsonFile<Record<string, ArtifactRecord>>(
|
|
102
|
+
this.indexPath,
|
|
103
|
+
{},
|
|
104
|
+
);
|
|
105
|
+
index[record.artifact_id] = record;
|
|
106
|
+
await writeJsonFile(this.indexPath, index);
|
|
107
|
+
return record;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ErrorType, ToolEnvelope } from "../schemas/common.js";
|
|
2
|
+
import { createToolFailure } from "../schemas/common.js";
|
|
3
|
+
|
|
4
|
+
export type WebAgentErrorCode =
|
|
5
|
+
| "INPUT_INVALID_URL"
|
|
6
|
+
| "INPUT_MISSING_SESSION"
|
|
7
|
+
| "STATE_PAGE_NOT_FOUND"
|
|
8
|
+
| "STATE_NAVIGATION_NOT_OBSERVED"
|
|
9
|
+
| "STATE_ELEMENT_NOT_FOUND"
|
|
10
|
+
| "STATE_TARGET_NOT_INTERACTABLE"
|
|
11
|
+
| "BROWSER_LAUNCH_FAILED"
|
|
12
|
+
| "BROWSER_DISCONNECTED"
|
|
13
|
+
| "NETWORK_TIMEOUT"
|
|
14
|
+
| "STORAGE_WRITE_FAILED"
|
|
15
|
+
| "INTERNAL_NOT_IMPLEMENTED"
|
|
16
|
+
| "INTERNAL_UNREACHABLE";
|
|
17
|
+
|
|
18
|
+
const errorTypeByCode: Record<WebAgentErrorCode, ErrorType> = {
|
|
19
|
+
INPUT_INVALID_URL: "INPUT",
|
|
20
|
+
INPUT_MISSING_SESSION: "INPUT",
|
|
21
|
+
STATE_PAGE_NOT_FOUND: "STATE",
|
|
22
|
+
STATE_NAVIGATION_NOT_OBSERVED: "STATE",
|
|
23
|
+
STATE_ELEMENT_NOT_FOUND: "STATE",
|
|
24
|
+
STATE_TARGET_NOT_INTERACTABLE: "STATE",
|
|
25
|
+
BROWSER_LAUNCH_FAILED: "BROWSER",
|
|
26
|
+
BROWSER_DISCONNECTED: "BROWSER",
|
|
27
|
+
NETWORK_TIMEOUT: "NETWORK",
|
|
28
|
+
STORAGE_WRITE_FAILED: "STORAGE",
|
|
29
|
+
INTERNAL_NOT_IMPLEMENTED: "INTERNAL",
|
|
30
|
+
INTERNAL_UNREACHABLE: "INTERNAL",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class WebAgentError extends Error {
|
|
34
|
+
readonly code: WebAgentErrorCode;
|
|
35
|
+
readonly type: ErrorType;
|
|
36
|
+
readonly details?: Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
code: WebAgentErrorCode,
|
|
40
|
+
message: string,
|
|
41
|
+
details?: Record<string, unknown>,
|
|
42
|
+
) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "WebAgentError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.type = errorTypeByCode[code];
|
|
47
|
+
this.details = details;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function asWebAgentError(error: unknown) {
|
|
52
|
+
if (error instanceof WebAgentError) {
|
|
53
|
+
return error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const message =
|
|
57
|
+
error instanceof Error ? error.message : "Unexpected internal error.";
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
message.includes("locator(") ||
|
|
61
|
+
message.includes("waitForSelector") ||
|
|
62
|
+
message.includes("bounding box")
|
|
63
|
+
) {
|
|
64
|
+
if (message.toLowerCase().includes("timeout")) {
|
|
65
|
+
return new WebAgentError("STATE_ELEMENT_NOT_FOUND", message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
message.toLowerCase().includes("not visible") ||
|
|
70
|
+
message.toLowerCase().includes("not enabled")
|
|
71
|
+
) {
|
|
72
|
+
return new WebAgentError("STATE_TARGET_NOT_INTERACTABLE", message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return new WebAgentError("STATE_ELEMENT_NOT_FOUND", message);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
message
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.includes("target page, context or browser has been closed")
|
|
82
|
+
) {
|
|
83
|
+
return new WebAgentError("BROWSER_DISCONNECTED", message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (message.toLowerCase().includes("timeout")) {
|
|
87
|
+
return new WebAgentError("NETWORK_TIMEOUT", message);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return new WebAgentError("INTERNAL_UNREACHABLE", message);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createFailureResult(
|
|
94
|
+
error: unknown,
|
|
95
|
+
envelope: Partial<ToolEnvelope> = {},
|
|
96
|
+
) {
|
|
97
|
+
const mapped = asWebAgentError(error);
|
|
98
|
+
return createToolFailure({
|
|
99
|
+
ok: false,
|
|
100
|
+
code: mapped.code,
|
|
101
|
+
message: mapped.message,
|
|
102
|
+
...envelope,
|
|
103
|
+
error: {
|
|
104
|
+
type: mapped.type,
|
|
105
|
+
details: mapped.details,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { recommendObservation } from "./policy-engine.js";
|
|
2
|
+
|
|
3
|
+
type Goal = "interact" | "read" | "visual_verify" | "debug" | "gesture";
|
|
4
|
+
|
|
5
|
+
export function buildFollowUpByGoal(input: {
|
|
6
|
+
pageStateKnown: boolean;
|
|
7
|
+
targetSelectorKnown: boolean;
|
|
8
|
+
needsPreciseCoordinates?: boolean;
|
|
9
|
+
needsFullPageContext?: boolean;
|
|
10
|
+
recentFailure?: "none" | "semantic_not_found" | "semantic_failed" | "layout_uncertain" | "runtime_error";
|
|
11
|
+
}) {
|
|
12
|
+
const goals: Goal[] = ["interact", "read", "visual_verify", "debug", "gesture"];
|
|
13
|
+
|
|
14
|
+
return Object.fromEntries(
|
|
15
|
+
goals.map((goal) => [
|
|
16
|
+
goal,
|
|
17
|
+
toPolicyEnvelope(
|
|
18
|
+
recommendObservation({
|
|
19
|
+
goal,
|
|
20
|
+
page_state_known: input.pageStateKnown,
|
|
21
|
+
target_selector_known: input.targetSelectorKnown,
|
|
22
|
+
needs_precise_coordinates: goal === "gesture" ? true : Boolean(input.needsPreciseCoordinates),
|
|
23
|
+
needs_full_page_context: goal === "visual_verify" ? Boolean(input.needsFullPageContext) : false,
|
|
24
|
+
recent_failure: input.recentFailure ?? "none"
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
])
|
|
28
|
+
) as Record<Goal, ReturnType<typeof toPolicyEnvelope>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function toPolicyEnvelope(result: ReturnType<typeof recommendObservation>) {
|
|
32
|
+
return {
|
|
33
|
+
recommended_tool: result.recommendedTool,
|
|
34
|
+
screenshot_mode: result.screenshotMode,
|
|
35
|
+
rationale: result.rationale,
|
|
36
|
+
escalation_order: result.escalationOrder
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { RecommendObservationInput } from "../schemas/policy.js";
|
|
2
|
+
|
|
3
|
+
export type ObservationRecommendation = {
|
|
4
|
+
recommendedTool: "observe.a11y" | "observe.dom" | "observe.text" | "observe.boxes" | "observe.screenshot";
|
|
5
|
+
screenshotMode: "none" | "element" | "viewport" | "full";
|
|
6
|
+
rationale: string[];
|
|
7
|
+
escalationOrder: string[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function recommendObservation(input: RecommendObservationInput): ObservationRecommendation {
|
|
11
|
+
const rationale: string[] = [];
|
|
12
|
+
const escalationOrder = [
|
|
13
|
+
"observe.a11y",
|
|
14
|
+
"observe.dom",
|
|
15
|
+
"observe.text",
|
|
16
|
+
"observe.boxes",
|
|
17
|
+
"observe.screenshot"
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
if (input.goal === "read") {
|
|
21
|
+
rationale.push("Reading tasks should prefer cheap textual context before any visual artifact.");
|
|
22
|
+
return {
|
|
23
|
+
recommendedTool: "observe.text",
|
|
24
|
+
screenshotMode: "none",
|
|
25
|
+
rationale,
|
|
26
|
+
escalationOrder
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (input.goal === "debug") {
|
|
31
|
+
rationale.push("Debug flows usually need structural context before expensive screenshots.");
|
|
32
|
+
rationale.push(
|
|
33
|
+
input.recent_failure === "runtime_error"
|
|
34
|
+
? "Runtime errors often benefit from DOM plus later console/network review."
|
|
35
|
+
: "A screenshot is useful only after structural context is insufficient."
|
|
36
|
+
);
|
|
37
|
+
return {
|
|
38
|
+
recommendedTool: input.page_state_known ? "observe.dom" : "observe.a11y",
|
|
39
|
+
screenshotMode: "none",
|
|
40
|
+
rationale,
|
|
41
|
+
escalationOrder
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (input.goal === "visual_verify") {
|
|
46
|
+
rationale.push("Visual verification explicitly needs pixels, so screenshot escalation is justified.");
|
|
47
|
+
if (input.needs_full_page_context) {
|
|
48
|
+
rationale.push("Full-page context was requested, so prefer a full-page screenshot.");
|
|
49
|
+
return {
|
|
50
|
+
recommendedTool: "observe.screenshot",
|
|
51
|
+
screenshotMode: "full",
|
|
52
|
+
rationale,
|
|
53
|
+
escalationOrder
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
recommendedTool: "observe.screenshot",
|
|
59
|
+
screenshotMode: input.target_selector_known ? "element" : "viewport",
|
|
60
|
+
rationale,
|
|
61
|
+
escalationOrder
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (input.goal === "gesture" || input.needs_precise_coordinates) {
|
|
66
|
+
rationale.push("Gesture execution needs precise spatial data before acting.");
|
|
67
|
+
rationale.push("Bounding boxes are cheaper than screenshots and usually sufficient for coordinates.");
|
|
68
|
+
return {
|
|
69
|
+
recommendedTool: "observe.boxes",
|
|
70
|
+
screenshotMode: "none",
|
|
71
|
+
rationale,
|
|
72
|
+
escalationOrder
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (input.recent_failure === "semantic_not_found" || input.recent_failure === "semantic_failed") {
|
|
77
|
+
rationale.push("A semantic failure suggests spatial fallback is needed.");
|
|
78
|
+
return {
|
|
79
|
+
recommendedTool: "observe.boxes",
|
|
80
|
+
screenshotMode: "none",
|
|
81
|
+
rationale,
|
|
82
|
+
escalationOrder
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (input.goal === "interact") {
|
|
87
|
+
rationale.push("Interaction tasks should start with structured page state, not screenshots.");
|
|
88
|
+
if (input.page_state_known && input.target_selector_known) {
|
|
89
|
+
rationale.push("Known page state plus known target favors a lightweight DOM check.");
|
|
90
|
+
return {
|
|
91
|
+
recommendedTool: "observe.dom",
|
|
92
|
+
screenshotMode: "none",
|
|
93
|
+
rationale,
|
|
94
|
+
escalationOrder
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
recommendedTool: "observe.a11y",
|
|
100
|
+
screenshotMode: "none",
|
|
101
|
+
rationale,
|
|
102
|
+
escalationOrder
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
rationale.push("Default to structured accessibility context as the cheapest general-purpose observation.");
|
|
107
|
+
return {
|
|
108
|
+
recommendedTool: "observe.a11y",
|
|
109
|
+
screenshotMode: "none",
|
|
110
|
+
rationale,
|
|
111
|
+
escalationOrder
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { WebAgentError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export type RetryPolicyDecision = {
|
|
4
|
+
retryable: boolean;
|
|
5
|
+
maxAttempts: number;
|
|
6
|
+
retryHint?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function getRetryPolicy(error: WebAgentError): RetryPolicyDecision {
|
|
10
|
+
switch (error.code) {
|
|
11
|
+
case "STATE_ELEMENT_NOT_FOUND":
|
|
12
|
+
return {
|
|
13
|
+
retryable: true,
|
|
14
|
+
maxAttempts: 2,
|
|
15
|
+
retryHint:
|
|
16
|
+
"Refresh observation first, then retry with observe.boxes or an element screenshot.",
|
|
17
|
+
};
|
|
18
|
+
case "STATE_TARGET_NOT_INTERACTABLE":
|
|
19
|
+
return {
|
|
20
|
+
retryable: true,
|
|
21
|
+
maxAttempts: 2,
|
|
22
|
+
retryHint: "Re-check layout and visibility before retrying the action.",
|
|
23
|
+
};
|
|
24
|
+
case "NETWORK_TIMEOUT":
|
|
25
|
+
return {
|
|
26
|
+
retryable: true,
|
|
27
|
+
maxAttempts: 2,
|
|
28
|
+
retryHint: "Retry once if the page is still loading or unstable.",
|
|
29
|
+
};
|
|
30
|
+
case "BROWSER_DISCONNECTED":
|
|
31
|
+
return {
|
|
32
|
+
retryable: true,
|
|
33
|
+
maxAttempts: 1,
|
|
34
|
+
retryHint: "Restart the session before retrying.",
|
|
35
|
+
};
|
|
36
|
+
default:
|
|
37
|
+
return {
|
|
38
|
+
retryable: false,
|
|
39
|
+
maxAttempts: 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|