mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(protocol): negotiate Trace v3 with v2 compatibility
Define Trace v3 wire types and generated schemas while keeping protocol 1.1 peers compatible with 1.0. Default record start still produces Trace v2, and only an explicit trace_version=3 request enables the new shape. Refs #90 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -27,19 +27,19 @@ function handshake(
|
||||
|
||||
describe("computeConnectedState (protocol-based compat)", () => {
|
||||
it("returns connected when protocol strings match", () => {
|
||||
expect(computeConnectedState(handshake("1.0", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({
|
||||
expect(computeConnectedState(handshake("1.1", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({
|
||||
kind: "connected",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns version_skew when daemon protocol minor is newer", () => {
|
||||
expect(computeConnectedState(handshake("1.1", "1.0"))).toEqual({
|
||||
expect(computeConnectedState(handshake("1.2", "1.0"))).toEqual({
|
||||
kind: "version_skew",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns version_skew when daemon protocol string differs but floor is satisfied", () => {
|
||||
expect(computeConnectedState(handshake("1", "1.0"))).toEqual({
|
||||
expect(computeConnectedState(handshake("1.1.0", "1.0"))).toEqual({
|
||||
kind: "version_skew",
|
||||
});
|
||||
});
|
||||
@@ -53,7 +53,7 @@ describe("computeConnectedState (protocol-based compat)", () => {
|
||||
});
|
||||
|
||||
it("rejects when extension is below daemon min_compatible_protocol", () => {
|
||||
const result = computeConnectedState(handshake("1.0", "1.5"));
|
||||
const result = computeConnectedState(handshake("1.1", "1.5"));
|
||||
expect(result.kind).toBe("rejected");
|
||||
if (result.kind === "rejected") {
|
||||
expect(result.reason).toContain("min_compatible_protocol");
|
||||
@@ -65,7 +65,7 @@ describe("computeConnectedState (protocol-based compat)", () => {
|
||||
const result = computeConnectedState({
|
||||
server: "browser-skill-daemon",
|
||||
version: "0.1.0",
|
||||
protocol_version: "1.0",
|
||||
protocol_version: "1.1",
|
||||
min_compatible_peer: "0.1.0",
|
||||
});
|
||||
expect(result).toEqual({ kind: "connected" });
|
||||
@@ -79,8 +79,14 @@ describe("computeConnectedState (protocol-based compat)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns version_skew when daemon protocol is 1.0 and floor is satisfied", () => {
|
||||
expect(computeConnectedState(handshake("1.0", "1.0"))).toEqual({
|
||||
kind: "version_skew",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects malformed daemon min_compatible_protocol with a daemon-floor reason", () => {
|
||||
const result = computeConnectedState(handshake("1.0", "not-a-protocol"));
|
||||
const result = computeConnectedState(handshake("1.1", "not-a-protocol"));
|
||||
expect(result.kind).toBe("rejected");
|
||||
if (result.kind === "rejected") {
|
||||
expect(result.reason).toContain("daemon min_compatible_protocol");
|
||||
@@ -242,11 +248,11 @@ describe("ConnectionController connectionEnabled", () => {
|
||||
const second = transport.send.mock.calls[1]?.[0] as { id: string };
|
||||
expect(second.id).not.toBe(first.id);
|
||||
|
||||
transport.emitMessage({ id: first.id, result: handshake("1.0", "1.0") });
|
||||
transport.emitMessage({ id: first.id, result: handshake("1.1", "1.0") });
|
||||
await Promise.resolve();
|
||||
expect(controller.snapshot().state).not.toBe("connected");
|
||||
|
||||
transport.emitMessage({ id: second.id, result: handshake("1.0", "1.0") });
|
||||
transport.emitMessage({ id: second.id, result: handshake("1.1", "1.0") });
|
||||
await vi.waitFor(() => expect(controller.snapshot().state).toBe("connected"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,7 +151,7 @@ describe("reduceTraceSteps", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps hover steps before menu clicks", () => {
|
||||
it("drops hover steps unsupported by historical v2 clients", () => {
|
||||
const { steps } = reduceTraceSteps(
|
||||
[
|
||||
{
|
||||
@@ -167,11 +167,11 @@ describe("reduceTraceSteps", () => {
|
||||
],
|
||||
"https://example.com/app",
|
||||
);
|
||||
expect(steps.map((s) => s.op)).toEqual(["hover", "click"]);
|
||||
expect(steps.map((s) => s.op)).toEqual(["click"]);
|
||||
expect(steps[0]).toMatchObject({
|
||||
op: "hover",
|
||||
op: "click",
|
||||
page: "p1",
|
||||
target: { name: "Account" },
|
||||
target: { name: "Profile" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export function shouldRecordPress(
|
||||
}
|
||||
|
||||
function shouldIncludeDraft(step: DraftTraceStep): boolean {
|
||||
if (step.op === "hover") return false;
|
||||
if (step.op === "press" && !shouldRecordPress(step.key, step.modifiers)) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -139,12 +140,7 @@ function toV2Step(
|
||||
effectForNavigation(step.navigated_to, urlToId),
|
||||
);
|
||||
case "hover":
|
||||
return {
|
||||
op: "hover",
|
||||
id,
|
||||
page,
|
||||
target: step.target,
|
||||
};
|
||||
return null;
|
||||
case "fill":
|
||||
return {
|
||||
op: "fill",
|
||||
|
||||
@@ -65,6 +65,11 @@ function deferredFakeTransport(): { transport: Transport; emit: (frame: Protocol
|
||||
}
|
||||
|
||||
describe("performHandshake", () => {
|
||||
it("advertises the protocol compatibility boundary", () => {
|
||||
expect(PROTOCOL_VERSION).toBe("1.1");
|
||||
expect(MIN_COMPATIBLE_PROTOCOL).toBe("1.0");
|
||||
});
|
||||
|
||||
it("sends system.handshake with identity and both compat fields", async () => {
|
||||
let sentFrame: ProtocolFrame | null = null;
|
||||
const transport = fakeTransport((req) => {
|
||||
@@ -74,7 +79,7 @@ describe("performHandshake", () => {
|
||||
result: {
|
||||
server: "browser-skill-daemon",
|
||||
version: "0.1.0",
|
||||
protocol_version: "1.0",
|
||||
protocol_version: "1.1",
|
||||
min_compatible_peer: "0.0.0",
|
||||
min_compatible_protocol: "1.0",
|
||||
},
|
||||
@@ -129,8 +134,8 @@ describe("performHandshake", () => {
|
||||
const response = {
|
||||
server: "browser-skill-daemon",
|
||||
version: "0.1.0",
|
||||
protocol_version: "1.0",
|
||||
min_compatible_protocol: "1.0",
|
||||
protocol_version: "1.1",
|
||||
min_compatible_protocol: "1.1",
|
||||
} satisfies HandshakeResult;
|
||||
const transport = fakeTransport((req) => ({
|
||||
id: (req as { id: string }).id,
|
||||
@@ -145,7 +150,7 @@ describe("performHandshake", () => {
|
||||
});
|
||||
|
||||
expect(outcome.result.min_compatible_peer).toBeUndefined();
|
||||
expect(outcome.result.min_compatible_protocol).toBe("1.0");
|
||||
expect(outcome.result.min_compatible_protocol).toBe("1.1");
|
||||
});
|
||||
|
||||
it("rejects when the daemon responds with an error", async () => {
|
||||
@@ -178,9 +183,9 @@ describe("performHandshake", () => {
|
||||
result: {
|
||||
server: "browser-skill-daemon",
|
||||
version: "0.1.0",
|
||||
protocol_version: "1.0",
|
||||
protocol_version: "1.1",
|
||||
min_compatible_peer: "0.0.0",
|
||||
min_compatible_protocol: "1.0",
|
||||
min_compatible_protocol: "1.1",
|
||||
},
|
||||
});
|
||||
emit({
|
||||
@@ -188,14 +193,14 @@ describe("performHandshake", () => {
|
||||
result: {
|
||||
server: "browser-skill-daemon",
|
||||
version: "0.1.0",
|
||||
protocol_version: "1.0",
|
||||
protocol_version: "1.1",
|
||||
min_compatible_peer: "0.0.0",
|
||||
min_compatible_protocol: "1.0",
|
||||
min_compatible_protocol: "1.1",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(pending).resolves.toMatchObject({
|
||||
result: { server: "browser-skill-daemon", protocol_version: "1.0" },
|
||||
result: { server: "browser-skill-daemon", protocol_version: "1.1" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
ResponseFrame,
|
||||
} from "./types";
|
||||
|
||||
export const PROTOCOL_VERSION = "1.0";
|
||||
export const PROTOCOL_VERSION = "1.1";
|
||||
/**
|
||||
* Extension semver, injected at build time from `package.json` via
|
||||
* Vite's `define` (see `wxt.config.ts` and `vitest.config.ts`).
|
||||
|
||||
@@ -657,9 +657,62 @@ export interface EmulateResult {
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Semantic record payloads — mirror bsk-protocol record.rs (Trace v2)
|
||||
// Semantic record payloads — mirror bsk-protocol record.rs
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
export const TRACE_VERSION = 3;
|
||||
export const TRACE_VERSION_V2 = 2;
|
||||
export const DEFAULT_TRACE_VERSION = 2;
|
||||
export const VOM_FORMAT_VERSION = 1;
|
||||
|
||||
export interface TargetDescriptorV3 {
|
||||
ref?: string;
|
||||
role?: string;
|
||||
name?: string;
|
||||
ctx?: string;
|
||||
unmatched?: boolean;
|
||||
}
|
||||
|
||||
export interface RecorderInfo {
|
||||
bsk: string;
|
||||
vom: number;
|
||||
}
|
||||
|
||||
export type StopReason = "user_finish" | "cli_stop";
|
||||
|
||||
export interface TraceState {
|
||||
id: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
/** Wire-only: full page observation text. */
|
||||
body?: string;
|
||||
/** Disk-only: filename under the bundle `pages/` directory. */
|
||||
page?: string;
|
||||
truncated?: boolean;
|
||||
}
|
||||
|
||||
export interface StepResult {
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface StepCommonV3 {
|
||||
id: number;
|
||||
state: string;
|
||||
result: StepResult;
|
||||
}
|
||||
|
||||
export type NavigationCause =
|
||||
| "user_typed"
|
||||
| "link"
|
||||
| "form_submit"
|
||||
| "reload"
|
||||
| "history"
|
||||
| "script"
|
||||
| "browser";
|
||||
|
||||
export type FillCommit = "enter" | "suggestion" | "blur";
|
||||
|
||||
/** Legacy v2 target shape retained for existing record producers. */
|
||||
export interface TargetDescriptor {
|
||||
role?: string;
|
||||
name?: string;
|
||||
@@ -740,7 +793,6 @@ export type DraftTraceStep =
|
||||
export type Step =
|
||||
| ({ op: "navigate" } & StepCommon & { to: string })
|
||||
| ({ op: "click" } & StepCommon & { target: TargetDescriptor })
|
||||
| ({ op: "hover" } & StepCommon & { target: TargetDescriptor })
|
||||
| ({ op: "fill" } & StepCommon & {
|
||||
target: TargetDescriptor;
|
||||
value: string;
|
||||
@@ -756,6 +808,9 @@ export type Step =
|
||||
target?: TargetDescriptor;
|
||||
});
|
||||
|
||||
export type TargetDescriptorV2 = TargetDescriptor;
|
||||
export type StepV2 = Step;
|
||||
|
||||
export interface Trace {
|
||||
recorded_at: string;
|
||||
started_at?: string;
|
||||
@@ -765,11 +820,52 @@ export interface Trace {
|
||||
steps: Step[];
|
||||
}
|
||||
|
||||
export type TraceV2 = Trace;
|
||||
|
||||
export type StepV3 =
|
||||
| ({ op: "navigate" } & StepCommonV3 & { to: string; cause: NavigationCause })
|
||||
| ({ op: "click" } & StepCommonV3 & { target: TargetDescriptorV3 })
|
||||
| ({ op: "hover" } & StepCommonV3 & { target: TargetDescriptorV3 })
|
||||
| ({ op: "fill" } & StepCommonV3 & {
|
||||
target: TargetDescriptorV3;
|
||||
value: string;
|
||||
commit: FillCommit;
|
||||
redacted?: boolean;
|
||||
})
|
||||
| ({ op: "select" } & StepCommonV3 & {
|
||||
target: TargetDescriptorV3;
|
||||
selection: SelectedOption[];
|
||||
})
|
||||
| ({ op: "press" } & StepCommonV3 & {
|
||||
key: string;
|
||||
modifiers?: KeyModifier[];
|
||||
target?: TargetDescriptorV3;
|
||||
})
|
||||
| ({ op: "scroll" } & StepCommonV3);
|
||||
|
||||
export interface TraceV3 {
|
||||
version: number;
|
||||
recorded_at: string;
|
||||
started_at?: string;
|
||||
purpose?: string;
|
||||
stopped_by: StopReason;
|
||||
entry: TraceEntry;
|
||||
recorder: RecorderInfo;
|
||||
states: TraceState[];
|
||||
steps: StepV3[];
|
||||
}
|
||||
|
||||
export type RecordedTrace = TraceV2 | TraceV3;
|
||||
|
||||
export interface RecordStartParams {
|
||||
session_id: string;
|
||||
tab_id?: number;
|
||||
url?: string;
|
||||
purpose?: string;
|
||||
max_page_tokens?: number;
|
||||
redact_values?: boolean;
|
||||
/** Omitted means v2; `3` requests a state-linked v3 trace. */
|
||||
trace_version?: number;
|
||||
}
|
||||
|
||||
export interface RecordStartResult {
|
||||
@@ -782,7 +878,7 @@ export interface RecordStopParams {
|
||||
}
|
||||
|
||||
export interface RecordStopResult {
|
||||
trace: Trace;
|
||||
trace: RecordedTrace;
|
||||
}
|
||||
|
||||
export interface RecordAwaitParams {
|
||||
@@ -791,5 +887,5 @@ export interface RecordAwaitParams {
|
||||
}
|
||||
|
||||
export interface RecordAwaitResult {
|
||||
trace: Trace;
|
||||
trace: RecordedTrace;
|
||||
}
|
||||
|
||||
@@ -486,7 +486,7 @@ mod m2_tests {
|
||||
fn fake_status(browsers: Vec<BrowserStatusEntry>, skew: Vec<VersionSkewEntry>) -> StatusResult {
|
||||
StatusResult {
|
||||
daemon_version: env!("CARGO_PKG_VERSION").into(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
pid: 1,
|
||||
uptime_secs: 0,
|
||||
ws_port: 0,
|
||||
@@ -571,7 +571,7 @@ mod m2_tests {
|
||||
session_count: 0,
|
||||
connected_at_ms: 1,
|
||||
version_skew: false,
|
||||
extension_protocol_version: "1.0".into(),
|
||||
extension_protocol_version: "1.1".into(),
|
||||
}],
|
||||
Vec::new(),
|
||||
);
|
||||
@@ -592,7 +592,7 @@ mod m2_tests {
|
||||
session_count: 0,
|
||||
connected_at_ms: 1,
|
||||
version_skew: true,
|
||||
extension_protocol_version: "1.1".into(),
|
||||
extension_protocol_version: "1.2".into(),
|
||||
}],
|
||||
vec![VersionSkewEntry {
|
||||
instance_id: "alpha".into(),
|
||||
@@ -600,8 +600,8 @@ mod m2_tests {
|
||||
label: "Personal".into(),
|
||||
server_version: env!("CARGO_PKG_VERSION").into(),
|
||||
client_version: "0.0.9".into(),
|
||||
server_protocol_version: "1.0".into(),
|
||||
client_protocol_version: "1.1".into(),
|
||||
server_protocol_version: "1.1".into(),
|
||||
client_protocol_version: "1.2".into(),
|
||||
}],
|
||||
);
|
||||
let check = check_browsers_protocol_compatible(Some(&status));
|
||||
|
||||
@@ -8,7 +8,7 @@ use anyhow::Context;
|
||||
use bsk_protocol::Method;
|
||||
use bsk_protocol::tools::{
|
||||
RecordAwaitParams, RecordAwaitResult, RecordStartParams, RecordStartResult, RecordStopParams,
|
||||
RecordStopResult, Trace,
|
||||
RecordStopResult, RecordedTrace,
|
||||
};
|
||||
use clap::{Args, Subcommand};
|
||||
|
||||
@@ -98,6 +98,9 @@ fn dispatch_start(args: RecordStartArgs, format: Format) -> Result<(), CliError>
|
||||
tab_id: args.tab_id,
|
||||
url: args.url,
|
||||
purpose: args.purpose.clone(),
|
||||
max_page_tokens: None,
|
||||
redact_values: None,
|
||||
trace_version: None,
|
||||
};
|
||||
let start_result = business_rpc::call::<RecordStartParams, RecordStartResult>(
|
||||
info.sock_path.clone(),
|
||||
@@ -190,7 +193,7 @@ fn record_await_ipc_timeout(timeout_ms: u32) -> Duration {
|
||||
.unwrap_or(Duration::from_secs(u64::from(timeout_ms / 1_000) + 15))
|
||||
}
|
||||
|
||||
fn write_trace_file(output: &PathBuf, trace: &Trace) -> Result<(), CliError> {
|
||||
fn write_trace_file(output: &PathBuf, trace: &RecordedTrace) -> Result<(), CliError> {
|
||||
let json = serde_json::to_string_pretty(trace)
|
||||
.context("serialize trace JSON")
|
||||
.map_err(CliError::Local)?;
|
||||
@@ -207,7 +210,7 @@ fn write_trace_file(output: &PathBuf, trace: &Trace) -> Result<(), CliError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_finish(trace: &Trace, output: &PathBuf, format: Format) -> Result<(), CliError> {
|
||||
fn render_finish(trace: &RecordedTrace, output: &PathBuf, format: Format) -> Result<(), CliError> {
|
||||
match format {
|
||||
Format::Json => {
|
||||
println!(
|
||||
@@ -221,7 +224,11 @@ fn render_finish(trace: &Trace, output: &PathBuf, format: Format) -> Result<(),
|
||||
);
|
||||
}
|
||||
Format::Human => {
|
||||
println!("saved {} steps to {}", trace.steps.len(), output.display());
|
||||
let step_count = match trace {
|
||||
RecordedTrace::V2(trace) => trace.steps.len(),
|
||||
RecordedTrace::V3(trace) => trace.steps.len(),
|
||||
};
|
||||
println!("saved {step_count} steps to {}", output.display());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::daemon::{
|
||||
browsers::{BROWSER_LIVENESS_TICK, BROWSER_LIVENESS_TIMEOUT, EXTENSION_CONNECT_WAIT},
|
||||
info as daemon_info, ipc, lockfile, paths,
|
||||
sessions::{StopSessionError, forget_session, stop_session},
|
||||
state::DaemonState,
|
||||
state::{DaemonState, PROTOCOL_VERSION},
|
||||
ws,
|
||||
};
|
||||
|
||||
@@ -297,7 +297,7 @@ pub fn run_foreground(cfg: DaemonConfig) -> Result<()> {
|
||||
ws_port,
|
||||
sock_path: sock_path.clone(),
|
||||
daemon_version: env!("CARGO_PKG_VERSION"),
|
||||
protocol_version: "1.0",
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
};
|
||||
let handler = ipc::full_handler(status, Arc::clone(&state));
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use super::start::DaemonConfig;
|
||||
use super::ws::WsHandle;
|
||||
|
||||
pub const DAEMON_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const PROTOCOL_VERSION: &str = "1.0";
|
||||
pub const PROTOCOL_VERSION: &str = "1.1";
|
||||
/// Lowest **protocol** version peers must speak (e.g. `"1.0"`).
|
||||
pub const MIN_COMPATIBLE_PROTOCOL: &str = "1.0";
|
||||
/// Legacy app-semver floor used only when `HandshakeResult.min_compatible_peer`
|
||||
|
||||
@@ -112,14 +112,14 @@ async fn handshake_as_ext(
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: TEST_EXT_ID.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
version: "131.0".into(),
|
||||
},
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
label: "Test".into(),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
|
||||
@@ -75,7 +75,7 @@ async fn handshake_as_ext(
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: TEST_EXT_ID.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
@@ -83,7 +83,7 @@ async fn handshake_as_ext(
|
||||
},
|
||||
label: "Test".into(),
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
id: "hs".into(),
|
||||
|
||||
@@ -108,12 +108,12 @@ async fn send_handshake_with_floors(
|
||||
async fn handshake_ok_when_protocol_matches() {
|
||||
let (handle, _sock) = spawn_daemon().await;
|
||||
let mut ws = open_ws(handle.ws_addr()).await;
|
||||
let resp = send_handshake(&mut ws, "1.0", env!("CARGO_PKG_VERSION")).await;
|
||||
let resp = send_handshake(&mut ws, "1.1", env!("CARGO_PKG_VERSION")).await;
|
||||
let result: HandshakeResult = match resp.body {
|
||||
ResponseBody::Ok(v) => serde_json::from_value(v).unwrap(),
|
||||
ResponseBody::Err(e) => panic!("expected ok handshake, got {e:?}"),
|
||||
};
|
||||
assert_eq!(result.protocol_version, "1.0");
|
||||
assert_eq!(result.protocol_version, "1.1");
|
||||
assert_eq!(
|
||||
result
|
||||
.min_compatible_peer
|
||||
@@ -135,7 +135,7 @@ async fn handshake_ok_when_app_versions_differ_but_protocol_matches() {
|
||||
let (handle, _sock) = spawn_daemon().await;
|
||||
let mut ws = open_ws(handle.ws_addr()).await;
|
||||
let resp =
|
||||
send_handshake_with_floors(&mut ws, "1.0", "9.9.9", Some("0.0.0"), Some("1.0")).await;
|
||||
send_handshake_with_floors(&mut ws, "1.1", "9.9.9", Some("0.0.0"), Some("1.1")).await;
|
||||
match resp.body {
|
||||
ResponseBody::Ok(_) => {}
|
||||
other => panic!("expected ok when protocol matches, got {other:?}"),
|
||||
@@ -149,7 +149,32 @@ async fn handshake_skew_when_protocol_minor_differs() {
|
||||
let mut ws = open_ws(handle.ws_addr()).await;
|
||||
let resp = send_handshake_with_floors(
|
||||
&mut ws,
|
||||
"1.1",
|
||||
"1.2",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
Some("0.0.0"),
|
||||
Some("1.1"),
|
||||
)
|
||||
.await;
|
||||
match resp.body {
|
||||
ResponseBody::Ok(_) => {}
|
||||
other => panic!("minor protocol drift should warn-but-allow, got {other:?}"),
|
||||
}
|
||||
let state = handle.state();
|
||||
let client = state
|
||||
.browsers
|
||||
.get(&bsk::daemon::browsers::BrowserId(TEST_EXT_ID.into()))
|
||||
.expect("browser registered");
|
||||
assert!(client.version_skew);
|
||||
handle.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handshake_skew_when_protocol_1_0_peer() {
|
||||
let (handle, _sock) = spawn_daemon().await;
|
||||
let mut ws = open_ws(handle.ws_addr()).await;
|
||||
let resp = send_handshake_with_floors(
|
||||
&mut ws,
|
||||
"1.0",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
Some("0.0.0"),
|
||||
Some("1.0"),
|
||||
@@ -157,7 +182,7 @@ async fn handshake_skew_when_protocol_minor_differs() {
|
||||
.await;
|
||||
match resp.body {
|
||||
ResponseBody::Ok(_) => {}
|
||||
other => panic!("minor protocol drift should warn-but-allow, got {other:?}"),
|
||||
other => panic!("protocol 1.0 peer should connect with skew, got {other:?}"),
|
||||
}
|
||||
let state = handle.state();
|
||||
let client = state
|
||||
@@ -210,7 +235,7 @@ async fn handshake_legacy_ext_without_protocol_floor_still_ok() {
|
||||
let mut ws = open_ws(handle.ws_addr()).await;
|
||||
let resp = send_handshake_with_floors(
|
||||
&mut ws,
|
||||
"1.0",
|
||||
"1.1",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
Some("0.1.0"),
|
||||
None,
|
||||
@@ -235,7 +260,7 @@ async fn status_surfaces_version_skew_for_skewed_browser() {
|
||||
browser_name: "chrome".into(),
|
||||
browser_version: "131.0".into(),
|
||||
extension_version: "9.9.9".into(),
|
||||
extension_protocol_version: "1.1".into(),
|
||||
extension_protocol_version: "1.2".into(),
|
||||
label: "Older".into(),
|
||||
sink: bsk::daemon::browsers::BrowserSink { tx },
|
||||
pending: Mutex::new(bsk::daemon::browsers::Pending::default()),
|
||||
@@ -263,8 +288,8 @@ async fn status_surfaces_version_skew_for_skewed_browser() {
|
||||
.iter()
|
||||
.find(|s| s.instance_id == "skew-only-test")
|
||||
.expect("status must list our skew client");
|
||||
assert_eq!(skew.client_protocol_version, "1.1");
|
||||
assert_eq!(skew.server_protocol_version, "1.0");
|
||||
assert_eq!(skew.client_protocol_version, "1.2");
|
||||
assert_eq!(skew.server_protocol_version, "1.1");
|
||||
assert_eq!(skew.client_version, "9.9.9");
|
||||
let entry = status
|
||||
.browsers
|
||||
@@ -281,7 +306,7 @@ async fn handshake_rejects_when_local_below_peer_min_compatible_protocol() {
|
||||
let mut ws = open_ws(handle.ws_addr()).await;
|
||||
let resp = send_handshake_with_floors(
|
||||
&mut ws,
|
||||
"1.0",
|
||||
"1.1",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
Some("0.0.0"),
|
||||
Some("99.0.0"),
|
||||
|
||||
@@ -81,14 +81,14 @@ async fn handshake_as_ext(
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: TEST_EXT_ID.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
version: "131.0".into(),
|
||||
},
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
label: "Test".into(),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
|
||||
@@ -81,7 +81,7 @@ async fn handshake_as_ext(
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: TEST_EXT_ID.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
@@ -89,7 +89,7 @@ async fn handshake_as_ext(
|
||||
},
|
||||
label: "Test".into(),
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
id: "hs".into(),
|
||||
|
||||
@@ -86,14 +86,14 @@ async fn handshake_as_ext(ws: &mut TestWs) -> HandshakeResult {
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: TEST_EXT_ID.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
version: "131.0".into(),
|
||||
},
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
label: "Test".into(),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
@@ -758,14 +758,14 @@ async fn connect_second_ext(
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: instance_id.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "edge".into(),
|
||||
version: "130".into(),
|
||||
},
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
label: label.into(),
|
||||
};
|
||||
let hs = RequestFrame {
|
||||
|
||||
@@ -61,7 +61,7 @@ fn bsk_status_json_returns_structured_payload() {
|
||||
|
||||
assert!(parsed["pid"].as_u64().unwrap() > 0);
|
||||
assert!(!parsed["daemon_version"].as_str().unwrap().is_empty());
|
||||
assert_eq!(parsed["protocol_version"], "1.0");
|
||||
assert_eq!(parsed["protocol_version"], "1.1");
|
||||
assert!(parsed["sock_path"].as_str().is_some());
|
||||
assert_eq!(parsed["browsers"], serde_json::json!([]));
|
||||
assert_eq!(parsed["sessions"], serde_json::json!([]));
|
||||
|
||||
@@ -73,14 +73,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult {
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: TEST_EXT_ID.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
version: "131.0".into(),
|
||||
},
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
label: "Test".into(),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
|
||||
@@ -76,14 +76,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult {
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: TEST_EXT_ID.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
version: "131.0".into(),
|
||||
},
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
label: "Test".into(),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
|
||||
@@ -75,14 +75,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult {
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: TEST_EXT_ID.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
version: "131.0".into(),
|
||||
},
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
label: "Test".into(),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
|
||||
@@ -85,14 +85,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult {
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: TEST_EXT_ID.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
version: "131.0".into(),
|
||||
},
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
label: "Test".into(),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
|
||||
@@ -53,14 +53,14 @@ pub async fn send_handshake(
|
||||
let params = HandshakeParams {
|
||||
client: "browser-skill-extension".into(),
|
||||
version: "0.1.0-dev.0".parse().unwrap(),
|
||||
protocol_version: "1.0".into(),
|
||||
protocol_version: "1.1".into(),
|
||||
instance_id: instance_id.into(),
|
||||
browser: BrowserPeerInfo {
|
||||
name: "chrome".into(),
|
||||
version: "131.0".into(),
|
||||
},
|
||||
min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()),
|
||||
min_compatible_protocol: Some("1.0".into()),
|
||||
min_compatible_protocol: Some("1.1".into()),
|
||||
label: "Test Chrome".into(),
|
||||
};
|
||||
let req = RequestFrame {
|
||||
@@ -91,7 +91,7 @@ async fn ws_handshake_registers_browser_in_state() {
|
||||
let mut ws = connect_ext(handle.ws_addr(), &origin).await;
|
||||
let result = send_handshake(&mut ws, TEST_EXT_ID).await;
|
||||
assert_eq!(result.server, "browser-skill-daemon");
|
||||
assert_eq!(result.protocol_version, "1.0");
|
||||
assert_eq!(result.protocol_version, "1.1");
|
||||
|
||||
let state = handle.state();
|
||||
let browsers = state.browsers.snapshot();
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
],
|
||||
"properties": {
|
||||
"trace": {
|
||||
"$ref": "#/definitions/Trace"
|
||||
"$ref": "#/definitions/RecordedTrace"
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"FillCommit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enter",
|
||||
"suggestion",
|
||||
"blur"
|
||||
]
|
||||
},
|
||||
"KeyModifier": {
|
||||
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
||||
"type": "string",
|
||||
@@ -21,6 +29,18 @@
|
||||
"shift"
|
||||
]
|
||||
},
|
||||
"NavigationCause": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user_typed",
|
||||
"link",
|
||||
"form_submit",
|
||||
"reload",
|
||||
"history",
|
||||
"script",
|
||||
"browser"
|
||||
]
|
||||
},
|
||||
"PageRef": {
|
||||
"description": "Page context dictionary entry — referenced by steps via `page` id.",
|
||||
"type": "object",
|
||||
@@ -43,6 +63,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"RecordedTrace": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TraceV2"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Trace"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RecorderInfo": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bsk",
|
||||
"vom"
|
||||
],
|
||||
"properties": {
|
||||
"bsk": {
|
||||
"type": "string"
|
||||
},
|
||||
"vom": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"SelectedOption": {
|
||||
"description": "One selected option (`select` op).",
|
||||
"type": "object",
|
||||
@@ -61,12 +108,333 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SelectedOptionV2": {
|
||||
"description": "One selected option (`select` op).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Step": {
|
||||
"description": "One recorded user action — discriminated union by `op`.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"cause",
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"to"
|
||||
],
|
||||
"properties": {
|
||||
"cause": {
|
||||
"$ref": "#/definitions/NavigationCause"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"navigate"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"click"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"hover"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"commit",
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"target",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"commit": {
|
||||
"$ref": "#/definitions/FillCommit"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"fill"
|
||||
]
|
||||
},
|
||||
"redacted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"select"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"selection": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SelectedOption"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"key",
|
||||
"op",
|
||||
"result",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"modifiers": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyModifier"
|
||||
}
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"press"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"scroll"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"StepEffectV2": {
|
||||
"description": "Observed navigation after a step (objective fact only).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"navigated_to"
|
||||
],
|
||||
"properties": {
|
||||
"navigated_to": {
|
||||
"description": "Reference into `pages[]` for the destination page.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StepResult": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StepV2": {
|
||||
"description": "One recorded user action — discriminated union by `op` (v2).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
@@ -77,7 +445,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -105,7 +473,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
@@ -117,7 +485,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -140,52 +508,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"hover"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
@@ -198,7 +526,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -227,7 +555,7 @@
|
||||
]
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
@@ -235,7 +563,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
@@ -248,7 +576,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -273,16 +601,16 @@
|
||||
"selection": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SelectedOption"
|
||||
"$ref": "#/definitions/SelectedOptionV2"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
@@ -294,7 +622,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -331,7 +659,7 @@
|
||||
"target": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -342,21 +670,48 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"StepEffect": {
|
||||
"description": "Observed navigation after a step (objective fact only).",
|
||||
"StopReason": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user_finish",
|
||||
"cli_stop"
|
||||
]
|
||||
},
|
||||
"TargetDescriptor": {
|
||||
"description": "Stable semantic handle for an interacted element within a page observation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"navigated_to"
|
||||
],
|
||||
"properties": {
|
||||
"navigated_to": {
|
||||
"description": "Reference into `pages[]` for the destination page.",
|
||||
"type": "string"
|
||||
"ctx": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"ref": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"unmatched": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TargetDescriptor": {
|
||||
"description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.",
|
||||
"TargetDescriptorV2": {
|
||||
"description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tag"
|
||||
@@ -400,6 +755,113 @@
|
||||
"Trace": {
|
||||
"description": "Persisted user-action trace exported by `tool.record_stop` / `await`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"entry",
|
||||
"recorded_at",
|
||||
"recorder",
|
||||
"states",
|
||||
"steps",
|
||||
"stopped_by",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"entry": {
|
||||
"$ref": "#/definitions/TraceEntry"
|
||||
},
|
||||
"purpose": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"recorded_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"recorder": {
|
||||
"$ref": "#/definitions/RecorderInfo"
|
||||
},
|
||||
"started_at": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"states": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TraceState"
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Step"
|
||||
}
|
||||
},
|
||||
"stopped_by": {
|
||||
"$ref": "#/definitions/StopReason"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceEntry": {
|
||||
"description": "Recording entry point — first URL the flow starts from.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"start_url"
|
||||
],
|
||||
"properties": {
|
||||
"start_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceState": {
|
||||
"description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"body": {
|
||||
"description": "Wire-only: full page observation (front matter + VOM body + annotations).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"description": "Disk-only: filename under the bundle `pages/` directory.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceV2": {
|
||||
"description": "Persisted user-action trace exported by legacy `tool.record_stop` / `await`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"entry",
|
||||
"pages",
|
||||
@@ -417,7 +879,6 @@
|
||||
}
|
||||
},
|
||||
"purpose": {
|
||||
"description": "Optional user-provided goal from `--purpose` (metadata only).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -436,22 +897,10 @@
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Step"
|
||||
"$ref": "#/definitions/StepV2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceEntry": {
|
||||
"description": "Recording entry point — first URL the flow starts from.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"start_url"
|
||||
],
|
||||
"properties": {
|
||||
"start_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,26 @@
|
||||
"session_id"
|
||||
],
|
||||
"properties": {
|
||||
"max_page_tokens": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"purpose": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"redact_values": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"session_id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -22,6 +36,15 @@
|
||||
],
|
||||
"format": "int64"
|
||||
},
|
||||
"trace_version": {
|
||||
"description": "Desired trace export format. Omitted ⇒ v2; `3` ⇒ state-linked v3 bundle.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"url": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
],
|
||||
"properties": {
|
||||
"trace": {
|
||||
"$ref": "#/definitions/Trace"
|
||||
"$ref": "#/definitions/RecordedTrace"
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"FillCommit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enter",
|
||||
"suggestion",
|
||||
"blur"
|
||||
]
|
||||
},
|
||||
"KeyModifier": {
|
||||
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
||||
"type": "string",
|
||||
@@ -21,6 +29,18 @@
|
||||
"shift"
|
||||
]
|
||||
},
|
||||
"NavigationCause": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user_typed",
|
||||
"link",
|
||||
"form_submit",
|
||||
"reload",
|
||||
"history",
|
||||
"script",
|
||||
"browser"
|
||||
]
|
||||
},
|
||||
"PageRef": {
|
||||
"description": "Page context dictionary entry — referenced by steps via `page` id.",
|
||||
"type": "object",
|
||||
@@ -43,6 +63,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"RecordedTrace": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TraceV2"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Trace"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RecorderInfo": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bsk",
|
||||
"vom"
|
||||
],
|
||||
"properties": {
|
||||
"bsk": {
|
||||
"type": "string"
|
||||
},
|
||||
"vom": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"SelectedOption": {
|
||||
"description": "One selected option (`select` op).",
|
||||
"type": "object",
|
||||
@@ -61,12 +108,333 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SelectedOptionV2": {
|
||||
"description": "One selected option (`select` op).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Step": {
|
||||
"description": "One recorded user action — discriminated union by `op`.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"cause",
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"to"
|
||||
],
|
||||
"properties": {
|
||||
"cause": {
|
||||
"$ref": "#/definitions/NavigationCause"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"navigate"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"click"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"hover"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"commit",
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"target",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"commit": {
|
||||
"$ref": "#/definitions/FillCommit"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"fill"
|
||||
]
|
||||
},
|
||||
"redacted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"select"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"selection": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SelectedOption"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"key",
|
||||
"op",
|
||||
"result",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"modifiers": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyModifier"
|
||||
}
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"press"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"scroll"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"StepEffectV2": {
|
||||
"description": "Observed navigation after a step (objective fact only).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"navigated_to"
|
||||
],
|
||||
"properties": {
|
||||
"navigated_to": {
|
||||
"description": "Reference into `pages[]` for the destination page.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StepResult": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StepV2": {
|
||||
"description": "One recorded user action — discriminated union by `op` (v2).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
@@ -77,7 +445,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -105,7 +473,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
@@ -117,7 +485,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -140,52 +508,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"hover"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
@@ -198,7 +526,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -227,7 +555,7 @@
|
||||
]
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
@@ -235,7 +563,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
@@ -248,7 +576,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -273,16 +601,16 @@
|
||||
"selection": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SelectedOption"
|
||||
"$ref": "#/definitions/SelectedOptionV2"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
@@ -294,7 +622,7 @@
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -331,7 +659,7 @@
|
||||
"target": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -342,21 +670,48 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"StepEffect": {
|
||||
"description": "Observed navigation after a step (objective fact only).",
|
||||
"StopReason": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user_finish",
|
||||
"cli_stop"
|
||||
]
|
||||
},
|
||||
"TargetDescriptor": {
|
||||
"description": "Stable semantic handle for an interacted element within a page observation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"navigated_to"
|
||||
],
|
||||
"properties": {
|
||||
"navigated_to": {
|
||||
"description": "Reference into `pages[]` for the destination page.",
|
||||
"type": "string"
|
||||
"ctx": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"ref": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"unmatched": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TargetDescriptor": {
|
||||
"description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.",
|
||||
"TargetDescriptorV2": {
|
||||
"description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tag"
|
||||
@@ -400,6 +755,113 @@
|
||||
"Trace": {
|
||||
"description": "Persisted user-action trace exported by `tool.record_stop` / `await`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"entry",
|
||||
"recorded_at",
|
||||
"recorder",
|
||||
"states",
|
||||
"steps",
|
||||
"stopped_by",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"entry": {
|
||||
"$ref": "#/definitions/TraceEntry"
|
||||
},
|
||||
"purpose": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"recorded_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"recorder": {
|
||||
"$ref": "#/definitions/RecorderInfo"
|
||||
},
|
||||
"started_at": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"states": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TraceState"
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Step"
|
||||
}
|
||||
},
|
||||
"stopped_by": {
|
||||
"$ref": "#/definitions/StopReason"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceEntry": {
|
||||
"description": "Recording entry point — first URL the flow starts from.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"start_url"
|
||||
],
|
||||
"properties": {
|
||||
"start_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceState": {
|
||||
"description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"body": {
|
||||
"description": "Wire-only: full page observation (front matter + VOM body + annotations).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"description": "Disk-only: filename under the bundle `pages/` directory.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceV2": {
|
||||
"description": "Persisted user-action trace exported by legacy `tool.record_stop` / `await`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"entry",
|
||||
"pages",
|
||||
@@ -417,7 +879,6 @@
|
||||
}
|
||||
},
|
||||
"purpose": {
|
||||
"description": "Optional user-provided goal from `--purpose` (metadata only).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -436,22 +897,10 @@
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Step"
|
||||
"$ref": "#/definitions/StepV2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceEntry": {
|
||||
"description": "Recording entry point — first URL the flow starts from.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"start_url"
|
||||
],
|
||||
"properties": {
|
||||
"start_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,45 +5,65 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"entry",
|
||||
"pages",
|
||||
"recorded_at",
|
||||
"steps"
|
||||
"recorder",
|
||||
"states",
|
||||
"steps",
|
||||
"stopped_by",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"entry": {
|
||||
"$ref": "#/definitions/TraceEntry"
|
||||
},
|
||||
"pages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PageRef"
|
||||
}
|
||||
},
|
||||
"purpose": {
|
||||
"description": "Optional user-provided goal from `--purpose` (metadata only).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"recorded_at": {
|
||||
"description": "RFC 3339 timestamp when recording stopped.",
|
||||
"type": "string"
|
||||
},
|
||||
"recorder": {
|
||||
"$ref": "#/definitions/RecorderInfo"
|
||||
},
|
||||
"started_at": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"states": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TraceState"
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Step"
|
||||
}
|
||||
},
|
||||
"stopped_by": {
|
||||
"$ref": "#/definitions/StopReason"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"FillCommit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enter",
|
||||
"suggestion",
|
||||
"blur"
|
||||
]
|
||||
},
|
||||
"KeyModifier": {
|
||||
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
||||
"type": "string",
|
||||
@@ -54,25 +74,32 @@
|
||||
"shift"
|
||||
]
|
||||
},
|
||||
"PageRef": {
|
||||
"description": "Page context dictionary entry — referenced by steps via `page` id.",
|
||||
"NavigationCause": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user_typed",
|
||||
"link",
|
||||
"form_submit",
|
||||
"reload",
|
||||
"history",
|
||||
"script",
|
||||
"browser"
|
||||
]
|
||||
},
|
||||
"RecorderInfo": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"url"
|
||||
"bsk",
|
||||
"vom"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"bsk": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
"vom": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -101,21 +128,16 @@
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"cause",
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"result",
|
||||
"state",
|
||||
"to"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
"cause": {
|
||||
"$ref": "#/definitions/NavigationCause"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
@@ -128,8 +150,11 @@
|
||||
"navigate"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
@@ -143,20 +168,11 @@
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
@@ -168,8 +184,11 @@
|
||||
"click"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
@@ -183,20 +202,11 @@
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
@@ -208,8 +218,11 @@
|
||||
"hover"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
@@ -221,22 +234,17 @@
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"commit",
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"result",
|
||||
"state",
|
||||
"target",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
"commit": {
|
||||
"$ref": "#/definitions/FillCommit"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
@@ -249,15 +257,15 @@
|
||||
"fill"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
},
|
||||
"redacted": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
@@ -273,21 +281,11 @@
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"selection",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
@@ -299,9 +297,8 @@
|
||||
"select"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"selection": {
|
||||
"type": "array",
|
||||
@@ -309,6 +306,10 @@
|
||||
"$ref": "#/definitions/SelectedOption"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
@@ -321,19 +322,10 @@
|
||||
"id",
|
||||
"key",
|
||||
"op",
|
||||
"page"
|
||||
"result",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
@@ -357,8 +349,11 @@
|
||||
"press"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
@@ -372,48 +367,74 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"scroll"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"StepEffect": {
|
||||
"description": "Observed navigation after a step (objective fact only).",
|
||||
"StepResult": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"navigated_to"
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"navigated_to": {
|
||||
"description": "Reference into `pages[]` for the destination page.",
|
||||
"state": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StopReason": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user_finish",
|
||||
"cli_stop"
|
||||
]
|
||||
},
|
||||
"TargetDescriptor": {
|
||||
"description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.",
|
||||
"description": "Stable semantic handle for an interacted element within a page observation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tag"
|
||||
],
|
||||
"properties": {
|
||||
"ctx": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name_attr": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"nearby_label": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"placeholder": {
|
||||
"ref": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -425,8 +446,8 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
"unmatched": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -441,6 +462,45 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceState": {
|
||||
"description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"body": {
|
||||
"description": "Wire-only: full page observation (front matter + VOM body + annotations).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"description": "Disk-only: filename under the bundle `pages/` directory.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,16 @@
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"cause",
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"result",
|
||||
"state",
|
||||
"to"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
"cause": {
|
||||
"$ref": "#/definitions/NavigationCause"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
@@ -34,8 +29,11 @@
|
||||
"navigate"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
@@ -49,20 +47,11 @@
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
@@ -74,8 +63,11 @@
|
||||
"click"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
@@ -89,20 +81,11 @@
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
@@ -114,8 +97,11 @@
|
||||
"hover"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
@@ -127,22 +113,17 @@
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"commit",
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"result",
|
||||
"state",
|
||||
"target",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
"commit": {
|
||||
"$ref": "#/definitions/FillCommit"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
@@ -155,15 +136,15 @@
|
||||
"fill"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
},
|
||||
"redacted": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
@@ -179,21 +160,11 @@
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"selection",
|
||||
"result",
|
||||
"state",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
@@ -205,9 +176,8 @@
|
||||
"select"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"selection": {
|
||||
"type": "array",
|
||||
@@ -215,6 +185,10 @@
|
||||
"$ref": "#/definitions/SelectedOption"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptor"
|
||||
}
|
||||
@@ -227,19 +201,10 @@
|
||||
"id",
|
||||
"key",
|
||||
"op",
|
||||
"page"
|
||||
"result",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
@@ -263,8 +228,11 @@
|
||||
"press"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
@@ -278,9 +246,47 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"result",
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"scroll"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/definitions/StepResult"
|
||||
},
|
||||
"state": {
|
||||
"description": "Observation id immediately before this action.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"FillCommit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enter",
|
||||
"suggestion",
|
||||
"blur"
|
||||
]
|
||||
},
|
||||
"KeyModifier": {
|
||||
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
||||
"type": "string",
|
||||
@@ -291,6 +297,18 @@
|
||||
"shift"
|
||||
]
|
||||
},
|
||||
"NavigationCause": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user_typed",
|
||||
"link",
|
||||
"form_submit",
|
||||
"reload",
|
||||
"history",
|
||||
"script",
|
||||
"browser"
|
||||
]
|
||||
},
|
||||
"SelectedOption": {
|
||||
"description": "One selected option (`select` op).",
|
||||
"type": "object",
|
||||
@@ -309,45 +327,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"StepEffect": {
|
||||
"description": "Observed navigation after a step (objective fact only).",
|
||||
"StepResult": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"navigated_to"
|
||||
"state"
|
||||
],
|
||||
"properties": {
|
||||
"navigated_to": {
|
||||
"description": "Reference into `pages[]` for the destination page.",
|
||||
"state": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TargetDescriptor": {
|
||||
"description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.",
|
||||
"description": "Stable semantic handle for an interacted element within a page observation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tag"
|
||||
],
|
||||
"properties": {
|
||||
"ctx": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name_attr": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"nearby_label": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"placeholder": {
|
||||
"ref": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -359,8 +366,8 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
"unmatched": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "TraceV2",
|
||||
"description": "Persisted user-action trace exported by legacy `tool.record_stop` / `await`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"entry",
|
||||
"pages",
|
||||
"recorded_at",
|
||||
"steps"
|
||||
],
|
||||
"properties": {
|
||||
"entry": {
|
||||
"$ref": "#/definitions/TraceEntry"
|
||||
},
|
||||
"pages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PageRef"
|
||||
}
|
||||
},
|
||||
"purpose": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"recorded_at": {
|
||||
"description": "RFC 3339 timestamp when recording stopped.",
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/StepV2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"KeyModifier": {
|
||||
"description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"alt",
|
||||
"ctrl",
|
||||
"meta",
|
||||
"shift"
|
||||
]
|
||||
},
|
||||
"PageRef": {
|
||||
"description": "Page context dictionary entry — referenced by steps via `page` id.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SelectedOptionV2": {
|
||||
"description": "One selected option (`select` op).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StepEffectV2": {
|
||||
"description": "Observed navigation after a step (objective fact only).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"navigated_to"
|
||||
],
|
||||
"properties": {
|
||||
"navigated_to": {
|
||||
"description": "Reference into `pages[]` for the destination page.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StepV2": {
|
||||
"description": "One recorded user action — discriminated union by `op` (v2).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"to"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"navigate"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"click"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"target",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"fill"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
},
|
||||
"redacted": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"op",
|
||||
"page",
|
||||
"selection",
|
||||
"target"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"select"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
},
|
||||
"selection": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SelectedOptionV2"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fields shared by every v2 step variant (flattened in JSON).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"key",
|
||||
"op",
|
||||
"page"
|
||||
],
|
||||
"properties": {
|
||||
"effect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StepEffectV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"modifiers": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyModifier"
|
||||
}
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"press"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Reference into `pages[]`.",
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TargetDescriptorV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"TargetDescriptorV2": {
|
||||
"description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tag"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name_attr": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"nearby_label": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"placeholder": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TraceEntry": {
|
||||
"description": "Recording entry point — first URL the flow starts from.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"start_url"
|
||||
],
|
||||
"properties": {
|
||||
"start_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,7 @@ fn main() {
|
||||
dump!(RequestHelpParams, "tool_request_help_params");
|
||||
dump!(RequestHelpResult, "tool_request_help_result");
|
||||
|
||||
dump!(TraceV2, "trace_v2");
|
||||
dump!(Trace, "trace");
|
||||
dump!(Step, "trace_step");
|
||||
dump!(RecordStartParams, "tool_record_start_params");
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod navigation;
|
||||
pub mod network;
|
||||
pub mod observation;
|
||||
pub mod record;
|
||||
pub mod record_v2;
|
||||
pub mod script;
|
||||
pub mod session;
|
||||
pub mod tabs;
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
//! Semantic user-action recording (`tool.record_start` / `stop` / `await`).
|
||||
//!
|
||||
//! Trace v2 is a **record-only** log of user actions: what was clicked, filled,
|
||||
//! selected, and where navigation occurred. No LLM runs during recording, so
|
||||
//! variable-vs-constant classification is **not** stored — executing agents
|
||||
//! infer that at run time from raw values and control names.
|
||||
//! Trace v3 is a **state-action-state** chain: each step binds to page
|
||||
//! observations (VOM) captured before and after the action.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::interaction::KeyModifier;
|
||||
|
||||
pub use crate::record_v2::{
|
||||
PageRef, SelectedOptionV2, StepCommonV2, StepEffectV2, StepV2, TargetDescriptorV2, TraceV2,
|
||||
};
|
||||
|
||||
pub const TRACE_VERSION: u32 = 3;
|
||||
pub const TRACE_VERSION_V2: u32 = 2;
|
||||
pub const DEFAULT_TRACE_VERSION: u32 = 2;
|
||||
pub const VOM_FORMAT_VERSION: u32 = 1;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Target
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Stable semantic handle for an interacted element.
|
||||
///
|
||||
/// `name` and `nearby_label` are **untrusted page text**.
|
||||
/// Stable semantic handle for an interacted element within a page observation.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TargetDescriptor {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none", rename = "ref")]
|
||||
pub element_ref: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub role: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
pub tag: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name_attr: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub placeholder: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nearby_label: Option<String>,
|
||||
pub ctx: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub unmatched: bool,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -42,15 +46,54 @@ pub struct TraceEntry {
|
||||
pub start_url: String,
|
||||
}
|
||||
|
||||
/// Page context dictionary entry — referenced by steps via `page` id.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PageRef {
|
||||
pub struct RecorderInfo {
|
||||
pub bsk: String,
|
||||
pub vom: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StopReason {
|
||||
UserFinish,
|
||||
CliStop,
|
||||
}
|
||||
|
||||
/// Page observation dictionary entry — referenced by steps via `state` / `result.state`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TraceState {
|
||||
pub id: String,
|
||||
pub url: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
/// Wire-only: full page observation (front matter + VOM body + annotations).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub body: Option<String>,
|
||||
/// Disk-only: filename under the bundle `pages/` directory.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub page: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StepResult {
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
/// Fields shared by every step variant (flattened in JSON).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StepCommon {
|
||||
pub id: u32,
|
||||
/// Observation id immediately before this action.
|
||||
pub state: String,
|
||||
pub result: StepResult,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step op-specific payloads
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One selected option (`select` op).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SelectedOption {
|
||||
@@ -59,27 +102,26 @@ pub struct SelectedOption {
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
/// Observed navigation after a step (objective fact only).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StepEffect {
|
||||
/// Reference into `pages[]` for the destination page.
|
||||
pub navigated_to: String,
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NavigationCause {
|
||||
UserTyped,
|
||||
Link,
|
||||
FormSubmit,
|
||||
Reload,
|
||||
History,
|
||||
Script,
|
||||
Browser,
|
||||
}
|
||||
|
||||
/// Fields shared by every step variant (flattened in JSON).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StepCommon {
|
||||
pub id: u32,
|
||||
/// Reference into `pages[]`.
|
||||
pub page: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub effect: Option<StepEffect>,
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FillCommit {
|
||||
Enter,
|
||||
Suggestion,
|
||||
Blur,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step op-specific payloads
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One recorded user action — discriminated union by `op`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "op", rename_all = "snake_case")]
|
||||
@@ -88,6 +130,7 @@ pub enum Step {
|
||||
#[serde(flatten)]
|
||||
common: StepCommon,
|
||||
to: String,
|
||||
cause: NavigationCause,
|
||||
},
|
||||
Click {
|
||||
#[serde(flatten)]
|
||||
@@ -104,13 +147,15 @@ pub enum Step {
|
||||
common: StepCommon,
|
||||
target: TargetDescriptor,
|
||||
value: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
redacted: Option<bool>,
|
||||
commit: FillCommit,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
redacted: bool,
|
||||
},
|
||||
Select {
|
||||
#[serde(flatten)]
|
||||
common: StepCommon,
|
||||
target: TargetDescriptor,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
selection: Vec<SelectedOption>,
|
||||
},
|
||||
Press {
|
||||
@@ -122,6 +167,10 @@ pub enum Step {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
target: Option<TargetDescriptor>,
|
||||
},
|
||||
Scroll {
|
||||
#[serde(flatten)]
|
||||
common: StepCommon,
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -131,15 +180,16 @@ pub enum Step {
|
||||
/// Persisted user-action trace exported by `tool.record_stop` / `await`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Trace {
|
||||
/// RFC 3339 timestamp when recording stopped.
|
||||
pub recorded_at: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub started_at: Option<String>,
|
||||
/// Optional user-provided goal from `--purpose` (metadata only).
|
||||
pub version: u32,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub purpose: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub started_at: Option<String>,
|
||||
pub recorded_at: String,
|
||||
pub stopped_by: StopReason,
|
||||
pub entry: TraceEntry,
|
||||
pub pages: Vec<PageRef>,
|
||||
pub recorder: RecorderInfo,
|
||||
pub states: Vec<TraceState>,
|
||||
pub steps: Vec<Step>,
|
||||
}
|
||||
|
||||
@@ -156,6 +206,13 @@ pub struct RecordStartParams {
|
||||
pub url: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub purpose: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_page_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub redact_values: Option<bool>,
|
||||
/// Desired trace export format. Omitted ⇒ v2; `3` ⇒ state-linked v3 bundle.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub trace_version: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -169,9 +226,100 @@ pub struct RecordStopParams {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
/// Wire trace payload — v2 (legacy `pages[]`) or v3 (`version: 3`, `states[]`).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RecordedTrace {
|
||||
V2(TraceV2),
|
||||
V3(Trace),
|
||||
}
|
||||
|
||||
impl RecordedTrace {
|
||||
pub fn classify_value(v: &serde_json::Value) -> Result<Self, String> {
|
||||
if let Some(ver) = v.get("version").and_then(|x| x.as_u64()) {
|
||||
if ver == u64::from(TRACE_VERSION) {
|
||||
return serde_json::from_value(v.clone())
|
||||
.map(RecordedTrace::V3)
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
return Err(format!("unsupported trace version {ver}"));
|
||||
}
|
||||
if v.get("pages").is_some() && v.get("states").is_none() {
|
||||
return serde_json::from_value(v.clone())
|
||||
.map(RecordedTrace::V2)
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
if v.get("states").is_some() {
|
||||
return serde_json::from_value(v.clone())
|
||||
.map(RecordedTrace::V3)
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
Err("ambiguous or unparseable trace".into())
|
||||
}
|
||||
|
||||
pub fn is_v3(&self) -> bool {
|
||||
matches!(self, RecordedTrace::V3(_))
|
||||
}
|
||||
|
||||
pub fn as_v3(&self) -> Option<&Trace> {
|
||||
match self {
|
||||
RecordedTrace::V3(t) => Some(t),
|
||||
RecordedTrace::V2(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_v2(&self) -> Option<&TraceV2> {
|
||||
match self {
|
||||
RecordedTrace::V2(t) => Some(t),
|
||||
RecordedTrace::V3(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for RecordedTrace {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
RecordedTrace::V2(t) => t.serialize(serializer),
|
||||
RecordedTrace::V3(t) => t.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RecordedTrace {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
Self::classify_value(&value).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for RecordedTrace {
|
||||
fn schema_name() -> String {
|
||||
"RecordedTrace".into()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
|
||||
one_of: Some(vec![
|
||||
generator.subschema_for::<TraceV2>(),
|
||||
generator.subschema_for::<Trace>(),
|
||||
]),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RecordStopResult {
|
||||
pub trace: Trace,
|
||||
pub trace: RecordedTrace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -183,7 +331,7 @@ pub struct RecordAwaitParams {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RecordAwaitResult {
|
||||
pub trace: Trace,
|
||||
pub trace: RecordedTrace,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -195,81 +343,79 @@ mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn sample_common(id: u32) -> StepCommon {
|
||||
fn sample_common(id: u32, state: &str, result_state: &str) -> StepCommon {
|
||||
StepCommon {
|
||||
id,
|
||||
page: "p1".into(),
|
||||
effect: None,
|
||||
state: state.into(),
|
||||
result: StepResult {
|
||||
state: result_state.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_target() -> TargetDescriptor {
|
||||
TargetDescriptor {
|
||||
element_ref: Some("e21".into()),
|
||||
role: Some("button".into()),
|
||||
name: Some("发布".into()),
|
||||
tag: "button".into(),
|
||||
name_attr: None,
|
||||
placeholder: None,
|
||||
nearby_label: None,
|
||||
ctx: Some("金桔柠檬 6 号".into()),
|
||||
unmatched: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_trace() -> Trace {
|
||||
Trace {
|
||||
recorded_at: "2026-07-17T09:01:10Z".into(),
|
||||
started_at: Some("2026-07-17T09:00:00Z".into()),
|
||||
purpose: Some("发布一篇文章".into()),
|
||||
version: TRACE_VERSION,
|
||||
purpose: Some("把草稿商品发布上架".into()),
|
||||
started_at: Some("2026-08-10T02:10:41.080Z".into()),
|
||||
recorded_at: "2026-08-10T02:12:55.360Z".into(),
|
||||
stopped_by: StopReason::UserFinish,
|
||||
entry: TraceEntry {
|
||||
start_url: "https://x.com/editor".into(),
|
||||
start_url: "https://example.com/".into(),
|
||||
},
|
||||
pages: vec![
|
||||
PageRef {
|
||||
id: "p1".into(),
|
||||
url: "https://x.com/editor".into(),
|
||||
title: Some("写文章".into()),
|
||||
recorder: RecorderInfo {
|
||||
bsk: "0.1.10".into(),
|
||||
vom: VOM_FORMAT_VERSION,
|
||||
},
|
||||
states: vec![
|
||||
TraceState {
|
||||
id: "s1".into(),
|
||||
url: "https://example.com/".into(),
|
||||
title: Some("Example Domain".into()),
|
||||
body: None,
|
||||
page: Some("s1.vom.txt".into()),
|
||||
truncated: false,
|
||||
},
|
||||
PageRef {
|
||||
id: "p2".into(),
|
||||
url: "https://x.com/p/99".into(),
|
||||
title: None,
|
||||
TraceState {
|
||||
id: "s2".into(),
|
||||
url: "https://shop.example.com/products?status=draft".into(),
|
||||
title: Some("商品管理".into()),
|
||||
body: None,
|
||||
page: Some("s2.vom.txt".into()),
|
||||
truncated: false,
|
||||
},
|
||||
],
|
||||
steps: vec![
|
||||
Step::Fill {
|
||||
common: sample_common(1),
|
||||
target: TargetDescriptor {
|
||||
role: Some("textbox".into()),
|
||||
name: Some("标题".into()),
|
||||
tag: "input".into(),
|
||||
name_attr: None,
|
||||
placeholder: None,
|
||||
nearby_label: None,
|
||||
},
|
||||
value: "我的第一篇文章".into(),
|
||||
redacted: None,
|
||||
Step::Navigate {
|
||||
common: sample_common(1, "s1", "s2"),
|
||||
to: "https://shop.example.com/products?status=draft".into(),
|
||||
cause: NavigationCause::UserTyped,
|
||||
},
|
||||
Step::Select {
|
||||
common: sample_common(3),
|
||||
Step::Fill {
|
||||
common: sample_common(2, "s2", "s2"),
|
||||
target: TargetDescriptor {
|
||||
role: Some("combobox".into()),
|
||||
name: Some("分类".into()),
|
||||
tag: "select".into(),
|
||||
name_attr: None,
|
||||
placeholder: None,
|
||||
nearby_label: None,
|
||||
element_ref: Some("e12".into()),
|
||||
role: Some("textbox".into()),
|
||||
name: Some("搜索商品".into()),
|
||||
ctx: None,
|
||||
unmatched: false,
|
||||
},
|
||||
selection: vec![SelectedOption {
|
||||
value: "tech".into(),
|
||||
label: Some("技术分享".into()),
|
||||
}],
|
||||
value: "金桔柠檬".into(),
|
||||
commit: FillCommit::Enter,
|
||||
redacted: false,
|
||||
},
|
||||
Step::Click {
|
||||
common: StepCommon {
|
||||
effect: Some(StepEffect {
|
||||
navigated_to: "p2".into(),
|
||||
}),
|
||||
..sample_common(4)
|
||||
},
|
||||
common: sample_common(3, "s2", "s2"),
|
||||
target: sample_target(),
|
||||
},
|
||||
],
|
||||
@@ -279,35 +425,37 @@ mod tests {
|
||||
#[test]
|
||||
fn step_click_round_trips() {
|
||||
let step = Step::Click {
|
||||
common: sample_common(1),
|
||||
common: sample_common(1, "s1", "s2"),
|
||||
target: sample_target(),
|
||||
};
|
||||
let v = serde_json::to_value(&step).unwrap();
|
||||
assert_eq!(v.get("op").and_then(|v| v.as_str()), Some("click"));
|
||||
assert_eq!(v.get("page").and_then(|v| v.as_str()), Some("p1"));
|
||||
assert!(v.get("key").is_none());
|
||||
assert_eq!(v.get("state").and_then(|v| v.as_str()), Some("s1"));
|
||||
assert_eq!(v["result"]["state"], "s2");
|
||||
assert_eq!(v["target"]["ref"], "e21");
|
||||
let round: Step = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(round, step);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_fill_with_raw_value_round_trips() {
|
||||
fn step_fill_with_commit_round_trips() {
|
||||
let step = Step::Fill {
|
||||
common: sample_common(2),
|
||||
common: sample_common(2, "s2", "s3"),
|
||||
target: TargetDescriptor {
|
||||
element_ref: Some("e12".into()),
|
||||
role: Some("textbox".into()),
|
||||
name: Some("服务名称".into()),
|
||||
tag: "input".into(),
|
||||
name_attr: Some("serviceName".into()),
|
||||
placeholder: None,
|
||||
nearby_label: None,
|
||||
name: Some("搜索商品".into()),
|
||||
ctx: None,
|
||||
unmatched: false,
|
||||
},
|
||||
value: "my-svc".into(),
|
||||
redacted: None,
|
||||
value: "browser skill".into(),
|
||||
commit: FillCommit::Enter,
|
||||
redacted: false,
|
||||
};
|
||||
let v = serde_json::to_value(&step).unwrap();
|
||||
assert_eq!(v["op"], "fill");
|
||||
assert_eq!(v["value"], "my-svc");
|
||||
assert_eq!(v["commit"], "enter");
|
||||
assert!(v.get("redacted").is_none());
|
||||
let round: Step = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(round, step);
|
||||
}
|
||||
@@ -315,17 +463,17 @@ mod tests {
|
||||
#[test]
|
||||
fn step_fill_password_is_redacted() {
|
||||
let step = Step::Fill {
|
||||
common: sample_common(1),
|
||||
common: sample_common(1, "s1", "s1"),
|
||||
target: TargetDescriptor {
|
||||
element_ref: Some("e3".into()),
|
||||
role: Some("textbox".into()),
|
||||
name: Some("密码".into()),
|
||||
tag: "input".into(),
|
||||
name_attr: None,
|
||||
placeholder: None,
|
||||
nearby_label: None,
|
||||
ctx: None,
|
||||
unmatched: false,
|
||||
},
|
||||
value: "***".into(),
|
||||
redacted: Some(true),
|
||||
commit: FillCommit::Blur,
|
||||
redacted: true,
|
||||
};
|
||||
let v = serde_json::to_value(&step).unwrap();
|
||||
assert_eq!(v["value"], "***");
|
||||
@@ -333,131 +481,140 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_select_uses_object_array() {
|
||||
let step = Step::Select {
|
||||
common: sample_common(3),
|
||||
target: TargetDescriptor {
|
||||
role: Some("combobox".into()),
|
||||
name: Some("分类".into()),
|
||||
tag: "select".into(),
|
||||
name_attr: None,
|
||||
placeholder: None,
|
||||
nearby_label: None,
|
||||
},
|
||||
selection: vec![SelectedOption {
|
||||
value: "tech".into(),
|
||||
label: Some("技术分享".into()),
|
||||
}],
|
||||
};
|
||||
let v = serde_json::to_value(&step).unwrap();
|
||||
assert_eq!(v["selection"][0]["value"], "tech");
|
||||
assert_eq!(v["selection"][0]["label"], "技术分享");
|
||||
let round: Step = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(round, step);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_navigate_round_trips() {
|
||||
fn step_navigate_with_cause_round_trips() {
|
||||
let step = Step::Navigate {
|
||||
common: sample_common(1),
|
||||
common: sample_common(1, "s1", "s2"),
|
||||
to: "https://example.com".into(),
|
||||
cause: NavigationCause::UserTyped,
|
||||
};
|
||||
let v = serde_json::to_value(&step).unwrap();
|
||||
assert_eq!(v["op"], "navigate");
|
||||
assert_eq!(v["to"], "https://example.com");
|
||||
assert_eq!(v["cause"], "user_typed");
|
||||
let round: Step = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(round, step);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_press_with_modifiers_round_trips() {
|
||||
let step = Step::Press {
|
||||
common: StepCommon {
|
||||
effect: Some(StepEffect {
|
||||
navigated_to: "p2".into(),
|
||||
}),
|
||||
..sample_common(2)
|
||||
},
|
||||
key: "Enter".into(),
|
||||
modifiers: Some(vec![KeyModifier::Ctrl, KeyModifier::Shift]),
|
||||
target: Some(sample_target()),
|
||||
};
|
||||
let v = serde_json::to_value(&step).unwrap();
|
||||
assert_eq!(v["key"], "Enter");
|
||||
assert_eq!(v["modifiers"], json!(["ctrl", "shift"]));
|
||||
assert_eq!(v["effect"]["navigated_to"], "p2");
|
||||
let round: Step = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(round, step);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_record_only_round_trips() {
|
||||
fn trace_v3_round_trips() {
|
||||
let trace = sample_trace();
|
||||
let v = serde_json::to_value(&trace).unwrap();
|
||||
assert!(v.get("version").is_none());
|
||||
assert_eq!(
|
||||
v.get("purpose").and_then(|v| v.as_str()),
|
||||
Some("发布一篇文章")
|
||||
);
|
||||
assert!(v.get("parameters").is_none());
|
||||
assert!(v.get("site").is_none());
|
||||
assert!(v.get("goal").is_none());
|
||||
assert_eq!(
|
||||
v.get("entry")
|
||||
.and_then(|e| e.get("start_url"))
|
||||
.and_then(|u| u.as_str()),
|
||||
Some("https://x.com/editor")
|
||||
);
|
||||
assert_eq!(v.get("version").and_then(|v| v.as_u64()), Some(3));
|
||||
assert!(v.get("pages").is_none());
|
||||
assert_eq!(v["recorder"]["vom"], 1);
|
||||
assert_eq!(v["states"].as_array().unwrap().len(), 2);
|
||||
let round: Trace = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(round, trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discriminated_union_click_ignores_extra_key_field() {
|
||||
let bad = json!({
|
||||
"op": "click",
|
||||
"id": 1,
|
||||
"page": "p1",
|
||||
"target": { "tag": "button", "name": "OK" },
|
||||
"key": "Enter"
|
||||
});
|
||||
let step: Step = serde_json::from_value(bad).unwrap();
|
||||
assert!(matches!(step, Step::Click { .. }));
|
||||
fn default_fields_are_omitted() {
|
||||
let step = Step::Click {
|
||||
common: sample_common(1, "s1", "s1"),
|
||||
target: TargetDescriptor {
|
||||
element_ref: Some("e1".into()),
|
||||
role: Some("button".into()),
|
||||
name: Some("OK".into()),
|
||||
ctx: None,
|
||||
unmatched: false,
|
||||
},
|
||||
};
|
||||
let v = serde_json::to_value(&step).unwrap();
|
||||
assert!(v.get("key").is_none());
|
||||
assert!(v.get("unmatched").is_none());
|
||||
assert!(v["target"].get("ctx").is_none());
|
||||
assert!(v["target"].get("unmatched").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmatched_target_serializes_flag() {
|
||||
let step = Step::Click {
|
||||
common: sample_common(1, "s1", "s2"),
|
||||
target: TargetDescriptor {
|
||||
element_ref: None,
|
||||
role: Some("button".into()),
|
||||
name: Some("发布".into()),
|
||||
ctx: None,
|
||||
unmatched: true,
|
||||
},
|
||||
};
|
||||
let v = serde_json::to_value(&step).unwrap();
|
||||
assert_eq!(v["target"]["unmatched"], true);
|
||||
assert!(v["target"].get("ref").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recorded_trace_classifies_v2_and_v3() {
|
||||
let v2 = json!({
|
||||
"recorded_at": "2026-07-21T08:00:00Z",
|
||||
"entry": { "start_url": "https://example.com/" },
|
||||
"pages": [{ "id": "p1", "url": "https://example.com/" }],
|
||||
"steps": []
|
||||
});
|
||||
match RecordedTrace::classify_value(&v2).unwrap() {
|
||||
RecordedTrace::V2(_) => {}
|
||||
other => panic!("expected v2, got {other:?}"),
|
||||
}
|
||||
|
||||
let v3 = json!({
|
||||
"version": 3,
|
||||
"recorded_at": "2026-07-21T08:00:00Z",
|
||||
"stopped_by": "user_finish",
|
||||
"entry": { "start_url": "https://example.com/" },
|
||||
"recorder": { "bsk": "0.1.10", "vom": 1 },
|
||||
"states": [],
|
||||
"steps": []
|
||||
});
|
||||
match RecordedTrace::classify_value(&v3).unwrap() {
|
||||
RecordedTrace::V3(t) => assert_eq!(t.version, 3),
|
||||
other => panic!("expected v3, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recorded_trace_schema_includes_v2_and_v3() {
|
||||
let schema = serde_json::to_value(schemars::schema_for!(RecordStopResult)).unwrap();
|
||||
let variants = schema["definitions"]["RecordedTrace"]["oneOf"]
|
||||
.as_array()
|
||||
.expect("RecordedTrace schema should use oneOf");
|
||||
|
||||
assert_eq!(variants.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_trace_deserializes() {
|
||||
let v = json!({
|
||||
"version": 3,
|
||||
"recorded_at": "2026-07-21T08:00:00Z",
|
||||
"started_at": "2026-07-21T07:59:00Z",
|
||||
"purpose": "demo",
|
||||
"stopped_by": "user_finish",
|
||||
"entry": { "start_url": "https://example.com/editor" },
|
||||
"pages": [
|
||||
{ "id": "p1", "url": "https://example.com/editor" },
|
||||
{ "id": "p2", "url": "https://example.com/p/99" }
|
||||
"recorder": { "bsk": "0.1.10", "vom": 1 },
|
||||
"states": [
|
||||
{ "id": "s1", "url": "https://example.com/editor", "page": "s1.vom.txt" },
|
||||
{ "id": "s2", "url": "https://example.com/p/99", "page": "s2.vom.txt" }
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"op": "fill",
|
||||
"id": 1,
|
||||
"page": "p1",
|
||||
"target": { "tag": "input", "role": "textbox", "name": "标题" },
|
||||
"value": "hello"
|
||||
"state": "s1",
|
||||
"result": { "state": "s1" },
|
||||
"target": { "ref": "e1", "role": "textbox", "name": "标题" },
|
||||
"value": "hello",
|
||||
"commit": "blur"
|
||||
},
|
||||
{
|
||||
"op": "click",
|
||||
"id": 2,
|
||||
"page": "p1",
|
||||
"target": { "tag": "button", "role": "button", "name": "发布" },
|
||||
"effect": { "navigated_to": "p2" }
|
||||
"state": "s1",
|
||||
"result": { "state": "s2" },
|
||||
"target": { "ref": "e2", "role": "button", "name": "发布" }
|
||||
}
|
||||
]
|
||||
});
|
||||
let trace: Trace = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(trace.entry.start_url, "https://example.com/editor");
|
||||
assert_eq!(trace.pages.len(), 2);
|
||||
assert_eq!(trace.version, 3);
|
||||
assert_eq!(trace.states.len(), 2);
|
||||
assert_eq!(trace.steps.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Trace v2 — record-only user-action log with `pages[]` and step `page` refs.
|
||||
//!
|
||||
//! Legacy wire format: no top-level `version` field. New peers omit
|
||||
//! `trace_version` on `record_start` to request this shape.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::interaction::KeyModifier;
|
||||
use super::record::TraceEntry;
|
||||
|
||||
/// Stable semantic handle for an interacted element (v2).
|
||||
///
|
||||
/// `name` and `nearby_label` are **untrusted page text**.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TargetDescriptorV2 {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub role: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
pub tag: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name_attr: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub placeholder: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nearby_label: Option<String>,
|
||||
}
|
||||
|
||||
/// Page context dictionary entry — referenced by steps via `page` id.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PageRef {
|
||||
pub id: String,
|
||||
pub url: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
/// One selected option (`select` op).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SelectedOptionV2 {
|
||||
pub value: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
/// Observed navigation after a step (objective fact only).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StepEffectV2 {
|
||||
/// Reference into `pages[]` for the destination page.
|
||||
pub navigated_to: String,
|
||||
}
|
||||
|
||||
/// Fields shared by every v2 step variant (flattened in JSON).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StepCommonV2 {
|
||||
pub id: u32,
|
||||
/// Reference into `pages[]`.
|
||||
pub page: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub effect: Option<StepEffectV2>,
|
||||
}
|
||||
|
||||
/// One recorded user action — discriminated union by `op` (v2).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "op", rename_all = "snake_case")]
|
||||
pub enum StepV2 {
|
||||
Navigate {
|
||||
#[serde(flatten)]
|
||||
common: StepCommonV2,
|
||||
to: String,
|
||||
},
|
||||
Click {
|
||||
#[serde(flatten)]
|
||||
common: StepCommonV2,
|
||||
target: TargetDescriptorV2,
|
||||
},
|
||||
Fill {
|
||||
#[serde(flatten)]
|
||||
common: StepCommonV2,
|
||||
target: TargetDescriptorV2,
|
||||
value: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
redacted: Option<bool>,
|
||||
},
|
||||
Select {
|
||||
#[serde(flatten)]
|
||||
common: StepCommonV2,
|
||||
target: TargetDescriptorV2,
|
||||
selection: Vec<SelectedOptionV2>,
|
||||
},
|
||||
Press {
|
||||
#[serde(flatten)]
|
||||
common: StepCommonV2,
|
||||
key: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
modifiers: Option<Vec<KeyModifier>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
target: Option<TargetDescriptorV2>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Persisted user-action trace exported by legacy `tool.record_stop` / `await`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TraceV2 {
|
||||
/// RFC 3339 timestamp when recording stopped.
|
||||
pub recorded_at: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub started_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub purpose: Option<String>,
|
||||
pub entry: TraceEntry,
|
||||
pub pages: Vec<PageRef>,
|
||||
pub steps: Vec<StepV2>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn sample_common(id: u32) -> StepCommonV2 {
|
||||
StepCommonV2 {
|
||||
id,
|
||||
page: "p1".into(),
|
||||
effect: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_target() -> TargetDescriptorV2 {
|
||||
TargetDescriptorV2 {
|
||||
role: Some("button".into()),
|
||||
name: Some("发布".into()),
|
||||
tag: "button".into(),
|
||||
name_attr: None,
|
||||
placeholder: None,
|
||||
nearby_label: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_v2_has_no_version_field() {
|
||||
let trace = TraceV2 {
|
||||
recorded_at: "2026-07-17T09:01:10Z".into(),
|
||||
started_at: None,
|
||||
purpose: None,
|
||||
entry: TraceEntry {
|
||||
start_url: "https://example.com/".into(),
|
||||
},
|
||||
pages: vec![PageRef {
|
||||
id: "p1".into(),
|
||||
url: "https://example.com/".into(),
|
||||
title: None,
|
||||
}],
|
||||
steps: vec![StepV2::Click {
|
||||
common: sample_common(1),
|
||||
target: sample_target(),
|
||||
}],
|
||||
};
|
||||
let v = serde_json::to_value(&trace).unwrap();
|
||||
assert!(v.get("version").is_none());
|
||||
assert!(v.get("pages").is_some());
|
||||
assert!(v.get("states").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_v2_trace_deserializes() {
|
||||
let v = json!({
|
||||
"recorded_at": "2026-07-21T08:00:00Z",
|
||||
"started_at": "2026-07-21T07:59:00Z",
|
||||
"purpose": "demo",
|
||||
"entry": { "start_url": "https://example.com/editor" },
|
||||
"pages": [
|
||||
{ "id": "p1", "url": "https://example.com/editor" },
|
||||
{ "id": "p2", "url": "https://example.com/p/99" }
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"op": "fill",
|
||||
"id": 1,
|
||||
"page": "p1",
|
||||
"target": { "tag": "input", "role": "textbox", "name": "标题" },
|
||||
"value": "hello"
|
||||
},
|
||||
{
|
||||
"op": "click",
|
||||
"id": 2,
|
||||
"page": "p1",
|
||||
"target": { "tag": "button", "role": "button", "name": "发布" },
|
||||
"effect": { "navigated_to": "p2" }
|
||||
}
|
||||
]
|
||||
});
|
||||
let trace: TraceV2 = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(trace.pages.len(), 2);
|
||||
assert_eq!(trace.steps.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_v2_rejects_hover_steps() {
|
||||
let value = json!({
|
||||
"recorded_at": "2026-07-21T08:00:00Z",
|
||||
"entry": { "start_url": "https://example.com/" },
|
||||
"pages": [{ "id": "p1", "url": "https://example.com/" }],
|
||||
"steps": [{
|
||||
"op": "hover",
|
||||
"id": 1,
|
||||
"page": "p1",
|
||||
"target": { "tag": "button", "name": "结束" }
|
||||
}]
|
||||
});
|
||||
|
||||
assert!(serde_json::from_value::<TraceV2>(value).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user