From b750eed5d4ef419db792d803b09caeb7ab75be9d Mon Sep 17 00:00:00 2001 From: Ljy-0827 Date: Wed, 19 Aug 2026 11:55:15 +0800 Subject: [PATCH] fix(protocol): integrate with vom --- .../__tests__/connection-controller.test.ts | 24 +- apps/extension/src/lib/trace-reducer.ts | 20 +- apps/extension/src/tools/record.ts | 18 +- .../src/transport/__tests__/handshake.test.ts | 23 +- apps/extension/src/transport/handshake.ts | 2 +- apps/extension/src/transport/types.ts | 85 +- crates/bsk-cli/src/cli/doctor.rs | 10 +- crates/bsk-cli/src/cli/record.rs | 3 - crates/bsk-cli/src/daemon/start.rs | 4 +- crates/bsk-cli/src/daemon/state.rs | 2 +- crates/bsk-cli/tests/browser_wait.rs | 2 +- crates/bsk-cli/tests/cancel_forwarding.rs | 2 +- crates/bsk-cli/tests/handshake_compat.rs | 45 +- crates/bsk-cli/tests/per_session_queue.rs | 2 +- .../bsk-cli/tests/session_user_interrupt.rs | 2 +- crates/bsk-cli/tests/sessions_ipc.rs | 4 +- crates/bsk-cli/tests/status_cmd.rs | 2 +- crates/bsk-cli/tests/tools_ipc.rs | 2 +- crates/bsk-cli/tests/tools_m7_ipc.rs | 2 +- crates/bsk-cli/tests/tools_m8_ipc.rs | 2 +- crates/bsk-cli/tests/tools_m9_ipc.rs | 2 +- crates/bsk-cli/tests/ws_handshake.rs | 4 +- .../schema/tool_record_await_result.json | 848 +++++++++--------- .../schema/tool_record_start_params.json | 23 - .../schema/tool_record_stop_result.json | 848 +++++++++--------- crates/bsk-protocol/schema/trace.json | 80 +- crates/bsk-protocol/schema/trace_step.json | 34 +- crates/bsk-protocol/schema/trace_v2.json | 5 +- crates/bsk-protocol/src/bin/dump-schema.rs | 4 +- crates/bsk-protocol/src/tools/mod.rs | 3 +- crates/bsk-protocol/src/tools/record.rs | 184 ++-- .../bsk-protocol/src/tools/record_common.rs | 7 + crates/bsk-protocol/src/tools/record_v2.rs | 11 +- 33 files changed, 1089 insertions(+), 1220 deletions(-) create mode 100644 crates/bsk-protocol/src/tools/record_common.rs diff --git a/apps/extension/src/lib/__tests__/connection-controller.test.ts b/apps/extension/src/lib/__tests__/connection-controller.test.ts index 4494523..6d5b578 100644 --- a/apps/extension/src/lib/__tests__/connection-controller.test.ts +++ b/apps/extension/src/lib/__tests__/connection-controller.test.ts @@ -26,20 +26,20 @@ function handshake( } describe("computeConnectedState (protocol-based compat)", () => { - it("returns connected when daemon protocol equals extension protocol", () => { - expect(computeConnectedState(handshake("1.1", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({ + it("returns connected when protocol strings match", () => { + expect(computeConnectedState(handshake("1.0", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({ kind: "connected", }); }); it("returns version_skew when daemon protocol minor is newer", () => { - expect(computeConnectedState(handshake("1.2", "1.0"))).toEqual({ + expect(computeConnectedState(handshake("1.1", "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", "1.0"))).toEqual({ + expect(computeConnectedState(handshake("1", "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.1", "1.5")); + const result = computeConnectedState(handshake("1.0", "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.1", + protocol_version: "1.0", min_compatible_peer: "0.1.0", }); expect(result).toEqual({ kind: "connected" }); @@ -79,14 +79,8 @@ 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.1", "not-a-protocol")); + const result = computeConnectedState(handshake("1.0", "not-a-protocol")); expect(result.kind).toBe("rejected"); if (result.kind === "rejected") { expect(result.reason).toContain("daemon min_compatible_protocol"); @@ -248,11 +242,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.1", "1.0") }); + transport.emitMessage({ id: first.id, result: handshake("1.0", "1.0") }); await Promise.resolve(); expect(controller.snapshot().state).not.toBe("connected"); - transport.emitMessage({ id: second.id, result: handshake("1.1", "1.0") }); + transport.emitMessage({ id: second.id, result: handshake("1.0", "1.0") }); await vi.waitFor(() => expect(controller.snapshot().state).toBe("connected")); }); }); diff --git a/apps/extension/src/lib/trace-reducer.ts b/apps/extension/src/lib/trace-reducer.ts index 8f83fff..b58b793 100644 --- a/apps/extension/src/lib/trace-reducer.ts +++ b/apps/extension/src/lib/trace-reducer.ts @@ -1,4 +1,4 @@ -import type { DraftTraceStep, PageRef, SelectedOption, Step } from "@/transport/types"; +import type { DraftTraceStep, PageRefV2, SelectedOptionV2, StepV2 } from "@/transport/types"; const CLIPBOARD_KEYS = new Set(["a", "c", "v", "x", "A", "C", "V", "X"]); const MODIFIER_ONLY_KEYS = new Set(["Meta", "Control", "Alt", "Shift", "OS", "Hyper", "Super"]); @@ -60,7 +60,7 @@ function collectUrls(steps: DraftTraceStep[], startUrl?: string): string[] { function buildPageRegistry( steps: DraftTraceStep[], startUrl?: string, -): { pages: PageRef[]; urlToId: Map } { +): { pages: PageRefV2[]; urlToId: Map } { const urls = collectUrls(steps, startUrl); const urlToId = new Map(); const pages = urls.map((url, index) => { @@ -90,19 +90,19 @@ function pageUrlForDraft(step: DraftTraceStep, fallbackUrl?: string): string | u function effectForNavigation( navigatedTo: string | undefined, urlToId: Map, -): Step["effect"] { +): StepV2["effect"] { if (!navigatedTo) return undefined; const pageId = urlToId.get(navigatedTo); if (!pageId) return undefined; return { navigated_to: pageId }; } -function withEffect(step: Step, effect: Step["effect"]): Step { +function withEffect(step: StepV2, effect: StepV2["effect"]): StepV2 { if (!effect) return step; return { ...step, effect }; } -function toSelection(values: string[], labels?: string[]): SelectedOption[] { +function toSelection(values: string[], labels?: string[]): SelectedOptionV2[] { return values.map((value, index) => ({ value, ...(labels?.[index] ? { label: labels[index] } : {}), @@ -114,7 +114,7 @@ function toV2Step( id: number, urlToId: Map, fallbackUrl?: string, -): Step | null { +): StepV2 | null { if (!shouldIncludeDraft(step)) return null; const pageUrl = pageUrlForDraft(step, fallbackUrl); @@ -181,8 +181,8 @@ function toV2Step( } export interface ReducedTrace { - pages: PageRef[]; - steps: Step[]; + pages: PageRefV2[]; + steps: StepV2[]; } /** @@ -192,7 +192,7 @@ export interface ReducedTrace { export function reduceTraceSteps(steps: DraftTraceStep[], startUrl?: string): ReducedTrace { const collapsed = collapseNavigations(steps); const { pages, urlToId } = buildPageRegistry(collapsed, startUrl); - const out: Step[] = []; + const out: StepV2[] = []; let id = 1; let lastUrl = startUrl; for (const draft of collapsed) { @@ -210,7 +210,7 @@ export function reduceTraceSteps(steps: DraftTraceStep[], startUrl?: string): Re export function resolveTraceStartUrl( drafts: DraftTraceStep[], startUrl?: string, - pages?: PageRef[], + pages?: PageRefV2[], ): string { if (startUrl) return startUrl; const navigate = drafts.find((step): step is Extract => { diff --git a/apps/extension/src/tools/record.ts b/apps/extension/src/tools/record.ts index c797ef1..be869ef 100644 --- a/apps/extension/src/tools/record.ts +++ b/apps/extension/src/tools/record.ts @@ -29,7 +29,7 @@ import type { RecordStopParams, RecordStopResult, RpcError, - Trace, + TraceV2, } from "@/transport/types"; import { handleNavigate } from "./navigation"; import { @@ -50,8 +50,8 @@ interface ActiveRecording { steps: DraftTraceStep[]; startedAt: string; startedAtMs: number; - finishPromise: Promise; - resolveFinish: (trace: Trace) => void; + finishPromise: Promise; + resolveFinish: (trace: TraceV2) => void; rejectFinish: (err: Error) => void; settled: boolean; finishing: boolean; @@ -154,7 +154,7 @@ async function sendRecordStartWithAck( throw lastError ?? new Error("failed to start recording in content script"); } -function buildTrace(recording: ActiveRecording): Trace { +function buildTrace(recording: ActiveRecording): TraceV2 { const { pages, steps } = reduceTraceSteps(recording.steps, recording.startUrl); const startUrl = resolveTraceStartUrl(recording.steps, recording.startUrl, pages); return { @@ -532,7 +532,7 @@ async function finishRecordingByRequest( } } -async function finishRecording(sessionId: string, deps: RecordDeps): Promise { +async function finishRecording(sessionId: string, deps: RecordDeps): Promise { const recording = recordings.get(sessionId); if (!recording || recording.settled || recording.finishing) return null; recording.finishing = true; @@ -574,9 +574,9 @@ export async function handleRecordStart( // on the destination page can RECORD_QUERY → rearm → show RecordOverlay // instead of flashing ControlOverlay ("Agent 正在控制"). const requestId = makeRequestId(target.tabId); - let resolveFinish!: (trace: Trace) => void; + let resolveFinish!: (trace: TraceV2) => void; let rejectFinish!: (err: Error) => void; - const finishPromise = new Promise((resolve, reject) => { + const finishPromise = new Promise((resolve, reject) => { resolveFinish = resolve; rejectFinish = reject; }); @@ -785,9 +785,9 @@ export async function handleRecordAwait( return { code: "cancelled", message: "record_await aborted" }; } - const outcome = await new Promise<{ trace: Trace } | { error: RpcError }>((resolve) => { + const outcome = await new Promise<{ trace: TraceV2 } | { error: RpcError }>((resolve) => { let settled = false; - const finish = (result: { trace: Trace } | { error: RpcError }) => { + const finish = (result: { trace: TraceV2 } | { error: RpcError }) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); diff --git a/apps/extension/src/transport/__tests__/handshake.test.ts b/apps/extension/src/transport/__tests__/handshake.test.ts index 6196f34..b511453 100644 --- a/apps/extension/src/transport/__tests__/handshake.test.ts +++ b/apps/extension/src/transport/__tests__/handshake.test.ts @@ -65,11 +65,6 @@ 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) => { @@ -79,7 +74,7 @@ describe("performHandshake", () => { result: { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.1", + protocol_version: "1.0", min_compatible_peer: "0.0.0", min_compatible_protocol: "1.0", }, @@ -134,8 +129,8 @@ describe("performHandshake", () => { const response = { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.1", - min_compatible_protocol: "1.1", + protocol_version: "1.0", + min_compatible_protocol: "1.0", } satisfies HandshakeResult; const transport = fakeTransport((req) => ({ id: (req as { id: string }).id, @@ -150,7 +145,7 @@ describe("performHandshake", () => { }); expect(outcome.result.min_compatible_peer).toBeUndefined(); - expect(outcome.result.min_compatible_protocol).toBe("1.1"); + expect(outcome.result.min_compatible_protocol).toBe("1.0"); }); it("rejects when the daemon responds with an error", async () => { @@ -183,9 +178,9 @@ describe("performHandshake", () => { result: { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.1", + protocol_version: "1.0", min_compatible_peer: "0.0.0", - min_compatible_protocol: "1.1", + min_compatible_protocol: "1.0", }, }); emit({ @@ -193,14 +188,14 @@ describe("performHandshake", () => { result: { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.1", + protocol_version: "1.0", min_compatible_peer: "0.0.0", - min_compatible_protocol: "1.1", + min_compatible_protocol: "1.0", }, }); await expect(pending).resolves.toMatchObject({ - result: { server: "browser-skill-daemon", protocol_version: "1.1" }, + result: { server: "browser-skill-daemon", protocol_version: "1.0" }, }); }); diff --git a/apps/extension/src/transport/handshake.ts b/apps/extension/src/transport/handshake.ts index a869ca5..3da56fd 100644 --- a/apps/extension/src/transport/handshake.ts +++ b/apps/extension/src/transport/handshake.ts @@ -7,7 +7,7 @@ import type { ResponseFrame, } from "./types"; -export const PROTOCOL_VERSION = "1.1"; +export const PROTOCOL_VERSION = "1.0"; /** * Extension semver, injected at build time from `package.json` via * Vite's `define` (see `wxt.config.ts` and `vitest.config.ts`). diff --git a/apps/extension/src/transport/types.ts b/apps/extension/src/transport/types.ts index e430a8e..5659392 100644 --- a/apps/extension/src/transport/types.ts +++ b/apps/extension/src/transport/types.ts @@ -657,12 +657,10 @@ export interface EmulateResult { } // -------------------------------------------------------------------------- -// Semantic record payloads — legacy Trace v2 shapes (`Trace`/`Step`) mirror -// bsk-protocol record_v2.rs; Trace v3 shapes (`TraceV3`/`StepV3`) mirror -// record.rs (note: in Rust, `Trace` is the v3 shape). +// Semantic record payloads mirror the versioned Rust protocol models. // -------------------------------------------------------------------------- -export const TRACE_VERSION = 3; +export const TRACE_VERSION_V3 = 3; export const TRACE_VERSION_V2 = 2; export const DEFAULT_TRACE_VERSION = 2; export const VOM_FORMAT_VERSION = 1; @@ -682,25 +680,22 @@ export interface RecorderInfo { export type StopReason = "user_finish" | "cli_stop"; -export interface TraceState { +export interface TraceStateV3 { id: string; url: string; title?: string; - /** Wire-only: full page observation text. */ - body?: string; - /** Disk-only: filename under the bundle `pages/` directory. */ - page?: string; + body: string; truncated?: boolean; } -export interface StepResult { +export interface StepResultV3 { state: string; } export interface StepCommonV3 { id: number; state: string; - result: StepResult; + result: StepResultV3; } export type NavigationCause = @@ -715,7 +710,7 @@ export type NavigationCause = export type FillCommit = "enter" | "suggestion" | "blur"; /** Legacy v2 target shape retained for existing record producers. */ -export interface TargetDescriptor { +export interface TargetDescriptorV2 { role?: string; name?: string; tag: string; @@ -728,43 +723,43 @@ export interface TraceEntry { start_url: string; } -export interface PageRef { +export interface PageRefV2 { id: string; url: string; title?: string; } -export interface SelectedOption { +export interface SelectedOptionV2 { value: string; label?: string; } -export interface StepEffect { +export interface StepEffectV2 { navigated_to: string; } -export interface StepCommon { +export interface StepCommonV2 { id: number; page: string; - effect?: StepEffect; + effect?: StepEffectV2; } /** Capture/buffer draft before v2 reduction. */ export type DraftTraceStep = | { op: "click"; - target: TargetDescriptor; + target: TargetDescriptorV2; navigated_to?: string; page_url?: string; } | { op: "hover"; - target: TargetDescriptor; + target: TargetDescriptorV2; page_url?: string; } | { op: "fill"; - target: TargetDescriptor; + target: TargetDescriptorV2; value: string; redacted?: boolean; page_url?: string; @@ -772,14 +767,14 @@ export type DraftTraceStep = | { op: "press"; key: string; - target?: TargetDescriptor; + target?: TargetDescriptorV2; modifiers?: KeyModifier[]; navigated_to?: string; page_url?: string; } | { op: "select"; - target: TargetDescriptor; + target: TargetDescriptorV2; values: string[]; labels?: string[]; navigated_to?: string; @@ -792,38 +787,38 @@ export type DraftTraceStep = }; /** Exported record-only step (trace v2). */ -export type Step = - | ({ op: "navigate" } & StepCommon & { to: string }) - | ({ op: "click" } & StepCommon & { target: TargetDescriptor }) - | ({ op: "hover" } & StepCommon & { target: TargetDescriptor }) - | ({ op: "fill" } & StepCommon & { - target: TargetDescriptor; +export type StepV2 = + | ({ op: "navigate" } & StepCommonV2 & { to: string }) + | ({ op: "click" } & StepCommonV2 & { target: TargetDescriptorV2 }) + | ({ op: "hover" } & StepCommonV2 & { target: TargetDescriptorV2 }) + | ({ op: "fill" } & StepCommonV2 & { + target: TargetDescriptorV2; value: string; redacted?: boolean; }) - | ({ op: "select" } & StepCommon & { - target: TargetDescriptor; - selection: SelectedOption[]; + | ({ op: "select" } & StepCommonV2 & { + target: TargetDescriptorV2; + selection: SelectedOptionV2[]; }) - | ({ op: "press" } & StepCommon & { + | ({ op: "press" } & StepCommonV2 & { key: string; modifiers?: KeyModifier[]; - target?: TargetDescriptor; + target?: TargetDescriptorV2; }); -export type TargetDescriptorV2 = TargetDescriptor; -export type StepV2 = Step; - -export interface Trace { +export interface TraceV2 { recorded_at: string; started_at?: string; purpose?: string; entry: TraceEntry; - pages: PageRef[]; - steps: Step[]; + pages: PageRefV2[]; + steps: StepV2[]; } -export type TraceV2 = Trace; +export interface SelectedOptionV3 { + value: string; + label?: string; +} export type StepV3 = | ({ op: "navigate" } & StepCommonV3 & { to: string; cause: NavigationCause }) @@ -837,7 +832,7 @@ export type StepV3 = }) | ({ op: "select" } & StepCommonV3 & { target: TargetDescriptorV3; - selection?: SelectedOption[]; + selection?: SelectedOptionV3[]; }) | ({ op: "press" } & StepCommonV3 & { key: string; @@ -847,14 +842,14 @@ export type StepV3 = | ({ op: "scroll" } & StepCommonV3); export interface TraceV3 { - version: 3; + version: typeof TRACE_VERSION_V3; recorded_at: string; started_at?: string; purpose?: string; stopped_by: StopReason; entry: TraceEntry; recorder: RecorderInfo; - states: TraceState[]; + states: TraceStateV3[]; steps: StepV3[]; } @@ -865,10 +860,6 @@ export interface RecordStartParams { 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 { diff --git a/crates/bsk-cli/src/cli/doctor.rs b/crates/bsk-cli/src/cli/doctor.rs index 2a83901..3d0c57a 100644 --- a/crates/bsk-cli/src/cli/doctor.rs +++ b/crates/bsk-cli/src/cli/doctor.rs @@ -486,7 +486,7 @@ mod m2_tests { fn fake_status(browsers: Vec, skew: Vec) -> StatusResult { StatusResult { daemon_version: env!("CARGO_PKG_VERSION").into(), - protocol_version: "1.1".into(), + protocol_version: "1.0".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.1".into(), + extension_protocol_version: "1.0".into(), }], Vec::new(), ); @@ -592,7 +592,7 @@ mod m2_tests { session_count: 0, connected_at_ms: 1, version_skew: true, - extension_protocol_version: "1.2".into(), + extension_protocol_version: "1.1".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.1".into(), - client_protocol_version: "1.2".into(), + server_protocol_version: "1.0".into(), + client_protocol_version: "1.1".into(), }], ); let check = check_browsers_protocol_compatible(Some(&status)); diff --git a/crates/bsk-cli/src/cli/record.rs b/crates/bsk-cli/src/cli/record.rs index 7cee47f..8cc66db 100644 --- a/crates/bsk-cli/src/cli/record.rs +++ b/crates/bsk-cli/src/cli/record.rs @@ -98,9 +98,6 @@ 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::( info.sock_path.clone(), diff --git a/crates/bsk-cli/src/daemon/start.rs b/crates/bsk-cli/src/daemon/start.rs index 241eb85..fdd46a6 100644 --- a/crates/bsk-cli/src/daemon/start.rs +++ b/crates/bsk-cli/src/daemon/start.rs @@ -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, PROTOCOL_VERSION}, + state::DaemonState, 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: PROTOCOL_VERSION, + protocol_version: "1.0", }; let handler = ipc::full_handler(status, Arc::clone(&state)); diff --git a/crates/bsk-cli/src/daemon/state.rs b/crates/bsk-cli/src/daemon/state.rs index ad3b48b..a1b5d11 100644 --- a/crates/bsk-cli/src/daemon/state.rs +++ b/crates/bsk-cli/src/daemon/state.rs @@ -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.1"; +pub const PROTOCOL_VERSION: &str = "1.0"; /// 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` diff --git a/crates/bsk-cli/tests/browser_wait.rs b/crates/bsk-cli/tests/browser_wait.rs index b2a9a14..16513e0 100644 --- a/crates/bsk-cli/tests/browser_wait.rs +++ b/crates/bsk-cli/tests/browser_wait.rs @@ -112,7 +112,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.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/cancel_forwarding.rs b/crates/bsk-cli/tests/cancel_forwarding.rs index 01894c8..c48113c 100644 --- a/crates/bsk-cli/tests/cancel_forwarding.rs +++ b/crates/bsk-cli/tests/cancel_forwarding.rs @@ -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.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/handshake_compat.rs b/crates/bsk-cli/tests/handshake_compat.rs index 7c8fd50..a5e91b0 100644 --- a/crates/bsk-cli/tests/handshake_compat.rs +++ b/crates/bsk-cli/tests/handshake_compat.rs @@ -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.1", env!("CARGO_PKG_VERSION")).await; + let resp = send_handshake(&mut ws, "1.0", 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.1"); + assert_eq!(result.protocol_version, "1.0"); 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.1", "9.9.9", Some("0.0.0"), Some("1.1")).await; + send_handshake_with_floors(&mut ws, "1.0", "9.9.9", Some("0.0.0"), Some("1.0")).await; match resp.body { ResponseBody::Ok(_) => {} other => panic!("expected ok when protocol matches, got {other:?}"), @@ -149,32 +149,7 @@ 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.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", + "1.1", env!("CARGO_PKG_VERSION"), Some("0.0.0"), Some("1.0"), @@ -182,7 +157,7 @@ async fn handshake_skew_when_protocol_1_0_peer() { .await; match resp.body { ResponseBody::Ok(_) => {} - other => panic!("protocol 1.0 peer should connect with skew, got {other:?}"), + other => panic!("minor protocol drift should warn-but-allow, got {other:?}"), } let state = handle.state(); let client = state @@ -235,7 +210,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.1", + "1.0", env!("CARGO_PKG_VERSION"), Some("0.1.0"), None, @@ -260,7 +235,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.2".into(), + extension_protocol_version: "1.1".into(), label: "Older".into(), sink: bsk::daemon::browsers::BrowserSink { tx }, pending: Mutex::new(bsk::daemon::browsers::Pending::default()), @@ -288,8 +263,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.2"); - assert_eq!(skew.server_protocol_version, "1.1"); + assert_eq!(skew.client_protocol_version, "1.1"); + assert_eq!(skew.server_protocol_version, "1.0"); assert_eq!(skew.client_version, "9.9.9"); let entry = status .browsers @@ -306,7 +281,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.1", + "1.0", env!("CARGO_PKG_VERSION"), Some("0.0.0"), Some("99.0.0"), diff --git a/crates/bsk-cli/tests/per_session_queue.rs b/crates/bsk-cli/tests/per_session_queue.rs index 8f0bdf6..70df3a7 100644 --- a/crates/bsk-cli/tests/per_session_queue.rs +++ b/crates/bsk-cli/tests/per_session_queue.rs @@ -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.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/session_user_interrupt.rs b/crates/bsk-cli/tests/session_user_interrupt.rs index 4ae1ce4..9c5288c 100644 --- a/crates/bsk-cli/tests/session_user_interrupt.rs +++ b/crates/bsk-cli/tests/session_user_interrupt.rs @@ -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.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/sessions_ipc.rs b/crates/bsk-cli/tests/sessions_ipc.rs index 4d55b06..e6d77a9 100644 --- a/crates/bsk-cli/tests/sessions_ipc.rs +++ b/crates/bsk-cli/tests/sessions_ipc.rs @@ -86,7 +86,7 @@ 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.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), @@ -758,7 +758,7 @@ async fn connect_second_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: instance_id.into(), browser: BrowserPeerInfo { name: "edge".into(), diff --git a/crates/bsk-cli/tests/status_cmd.rs b/crates/bsk-cli/tests/status_cmd.rs index 18513e2..0e32914 100644 --- a/crates/bsk-cli/tests/status_cmd.rs +++ b/crates/bsk-cli/tests/status_cmd.rs @@ -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.1"); + assert_eq!(parsed["protocol_version"], "1.0"); assert!(parsed["sock_path"].as_str().is_some()); assert_eq!(parsed["browsers"], serde_json::json!([])); assert_eq!(parsed["sessions"], serde_json::json!([])); diff --git a/crates/bsk-cli/tests/tools_ipc.rs b/crates/bsk-cli/tests/tools_ipc.rs index b64ff9b..d40bb9c 100644 --- a/crates/bsk-cli/tests/tools_ipc.rs +++ b/crates/bsk-cli/tests/tools_ipc.rs @@ -73,7 +73,7 @@ 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.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/tools_m7_ipc.rs b/crates/bsk-cli/tests/tools_m7_ipc.rs index 4635828..ee74b5e 100644 --- a/crates/bsk-cli/tests/tools_m7_ipc.rs +++ b/crates/bsk-cli/tests/tools_m7_ipc.rs @@ -76,7 +76,7 @@ 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.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/tools_m8_ipc.rs b/crates/bsk-cli/tests/tools_m8_ipc.rs index 777d212..14e7c72 100644 --- a/crates/bsk-cli/tests/tools_m8_ipc.rs +++ b/crates/bsk-cli/tests/tools_m8_ipc.rs @@ -75,7 +75,7 @@ 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.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/tools_m9_ipc.rs b/crates/bsk-cli/tests/tools_m9_ipc.rs index f5015ca..2f8b46d 100644 --- a/crates/bsk-cli/tests/tools_m9_ipc.rs +++ b/crates/bsk-cli/tests/tools_m9_ipc.rs @@ -85,7 +85,7 @@ 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.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/ws_handshake.rs b/crates/bsk-cli/tests/ws_handshake.rs index be413d2..c6e2da1 100644 --- a/crates/bsk-cli/tests/ws_handshake.rs +++ b/crates/bsk-cli/tests/ws_handshake.rs @@ -53,7 +53,7 @@ pub async fn send_handshake( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: instance_id.into(), browser: BrowserPeerInfo { name: "chrome".into(), @@ -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.1"); + assert_eq!(result.protocol_version, "1.0"); let state = handle.state(); let browsers = state.browsers.snapshot(); diff --git a/crates/bsk-protocol/schema/tool_record_await_result.json b/crates/bsk-protocol/schema/tool_record_await_result.json index a16fdf8..dee452f 100644 --- a/crates/bsk-protocol/schema/tool_record_await_result.json +++ b/crates/bsk-protocol/schema/tool_record_await_result.json @@ -41,7 +41,7 @@ "browser" ] }, - "PageRef": { + "PageRefV2": { "description": "Page context dictionary entry — referenced by steps via `page` id.", "type": "object", "required": [ @@ -69,7 +69,7 @@ "$ref": "#/definitions/TraceV2" }, { - "$ref": "#/definitions/Trace" + "$ref": "#/definitions/TraceV3" } ] }, @@ -90,24 +90,6 @@ } } }, - "SelectedOption": { - "description": "One selected option (`select` op).", - "type": "object", - "required": [ - "value" - ], - "properties": { - "label": { - "type": [ - "string", - "null" - ] - }, - "value": { - "type": "string" - } - } - }, "SelectedOptionV2": { "description": "One selected option (`select` op).", "type": "object", @@ -126,284 +108,23 @@ } } }, - "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" - } - } + "SelectedOptionV3": { + "description": "One selected option (`select` op).", + "type": "object", + "required": [ + "value" + ], + "properties": { + "label": { + "type": [ + "string", + "null" + ] }, - { - "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" - } - } + "value": { + "type": "string" } - ] + } }, "StepEffectV2": { "description": "Observed navigation after a step (objective fact only).", @@ -418,7 +139,7 @@ } } }, - "StepResult": { + "StepResultV3": { "type": "object", "required": [ "state" @@ -710,6 +431,285 @@ } ] }, + "StepV3": { + "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/StepResultV3" + }, + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" + } + } + }, + { + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" + } + } + }, + { + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" + }, + "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/StepResultV3" + }, + "selection": { + "type": "array", + "items": { + "$ref": "#/definitions/SelectedOptionV3" + } + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" + } + } + }, + { + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "anyOf": [ + { + "$ref": "#/definitions/TargetDescriptorV3" + }, + { + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + } + } + } + ] + }, "StopReason": { "type": "string", "enum": [ @@ -717,39 +717,6 @@ "cli_stop" ] }, - "TargetDescriptor": { - "description": "Stable semantic handle for an interacted element within a page observation.", - "type": "object", - "properties": { - "ctx": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": [ - "string", - "null" - ] - }, - "ref": { - "type": [ - "string", - "null" - ] - }, - "role": { - "type": [ - "string", - "null" - ] - }, - "unmatched": { - "type": "boolean" - } - } - }, "TargetDescriptorV2": { "description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.", "type": "object", @@ -792,8 +759,126 @@ } } }, - "Trace": { - "description": "Persisted user-action trace exported by `tool.record_stop` / `await`.", + "TargetDescriptorV3": { + "description": "Stable semantic handle for an interacted element within a page observation.", + "type": "object", + "properties": { + "ctx": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "ref": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": [ + "string", + "null" + ] + }, + "unmatched": { + "type": "boolean" + } + } + }, + "TraceEntry": { + "type": "object", + "required": [ + "start_url" + ], + "properties": { + "start_url": { + "type": "string" + } + } + }, + "TraceStateV3": { + "description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.", + "type": "object", + "required": [ + "body", + "id", + "url" + ], + "properties": { + "body": { + "description": "Full page observation (front matter + VOM body + annotations).", + "type": "string" + }, + "id": { + "type": "string" + }, + "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", + "recorded_at", + "steps" + ], + "properties": { + "entry": { + "$ref": "#/definitions/TraceEntry" + }, + "pages": { + "type": "array", + "items": { + "$ref": "#/definitions/PageRefV2" + } + }, + "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" + } + } + }, + "additionalProperties": false + }, + "TraceV3": { + "description": "Wire trace returned by `tool.record_stop` / `await`.", "type": "object", "required": [ "entry", @@ -829,13 +914,13 @@ "states": { "type": "array", "items": { - "$ref": "#/definitions/TraceState" + "$ref": "#/definitions/TraceStateV3" } }, "steps": { "type": "array", "items": { - "$ref": "#/definitions/Step" + "$ref": "#/definitions/StepV3" } }, "stopped_by": { @@ -847,101 +932,6 @@ } }, "additionalProperties": false - }, - "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", - "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" - } - } - }, - "additionalProperties": false } } } diff --git a/crates/bsk-protocol/schema/tool_record_start_params.json b/crates/bsk-protocol/schema/tool_record_start_params.json index e81c261..859ebf8 100644 --- a/crates/bsk-protocol/schema/tool_record_start_params.json +++ b/crates/bsk-protocol/schema/tool_record_start_params.json @@ -6,26 +6,12 @@ "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" }, @@ -36,15 +22,6 @@ ], "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", diff --git a/crates/bsk-protocol/schema/tool_record_stop_result.json b/crates/bsk-protocol/schema/tool_record_stop_result.json index 6fe6d48..5d1e04a 100644 --- a/crates/bsk-protocol/schema/tool_record_stop_result.json +++ b/crates/bsk-protocol/schema/tool_record_stop_result.json @@ -41,7 +41,7 @@ "browser" ] }, - "PageRef": { + "PageRefV2": { "description": "Page context dictionary entry — referenced by steps via `page` id.", "type": "object", "required": [ @@ -69,7 +69,7 @@ "$ref": "#/definitions/TraceV2" }, { - "$ref": "#/definitions/Trace" + "$ref": "#/definitions/TraceV3" } ] }, @@ -90,24 +90,6 @@ } } }, - "SelectedOption": { - "description": "One selected option (`select` op).", - "type": "object", - "required": [ - "value" - ], - "properties": { - "label": { - "type": [ - "string", - "null" - ] - }, - "value": { - "type": "string" - } - } - }, "SelectedOptionV2": { "description": "One selected option (`select` op).", "type": "object", @@ -126,284 +108,23 @@ } } }, - "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" - } - } + "SelectedOptionV3": { + "description": "One selected option (`select` op).", + "type": "object", + "required": [ + "value" + ], + "properties": { + "label": { + "type": [ + "string", + "null" + ] }, - { - "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" - } - } + "value": { + "type": "string" } - ] + } }, "StepEffectV2": { "description": "Observed navigation after a step (objective fact only).", @@ -418,7 +139,7 @@ } } }, - "StepResult": { + "StepResultV3": { "type": "object", "required": [ "state" @@ -710,6 +431,285 @@ } ] }, + "StepV3": { + "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/StepResultV3" + }, + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" + } + } + }, + { + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" + } + } + }, + { + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" + }, + "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/StepResultV3" + }, + "selection": { + "type": "array", + "items": { + "$ref": "#/definitions/SelectedOptionV3" + } + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" + } + } + }, + { + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "anyOf": [ + { + "$ref": "#/definitions/TargetDescriptorV3" + }, + { + "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/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + } + } + } + ] + }, "StopReason": { "type": "string", "enum": [ @@ -717,39 +717,6 @@ "cli_stop" ] }, - "TargetDescriptor": { - "description": "Stable semantic handle for an interacted element within a page observation.", - "type": "object", - "properties": { - "ctx": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": [ - "string", - "null" - ] - }, - "ref": { - "type": [ - "string", - "null" - ] - }, - "role": { - "type": [ - "string", - "null" - ] - }, - "unmatched": { - "type": "boolean" - } - } - }, "TargetDescriptorV2": { "description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.", "type": "object", @@ -792,8 +759,126 @@ } } }, - "Trace": { - "description": "Persisted user-action trace exported by `tool.record_stop` / `await`.", + "TargetDescriptorV3": { + "description": "Stable semantic handle for an interacted element within a page observation.", + "type": "object", + "properties": { + "ctx": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "ref": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": [ + "string", + "null" + ] + }, + "unmatched": { + "type": "boolean" + } + } + }, + "TraceEntry": { + "type": "object", + "required": [ + "start_url" + ], + "properties": { + "start_url": { + "type": "string" + } + } + }, + "TraceStateV3": { + "description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.", + "type": "object", + "required": [ + "body", + "id", + "url" + ], + "properties": { + "body": { + "description": "Full page observation (front matter + VOM body + annotations).", + "type": "string" + }, + "id": { + "type": "string" + }, + "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", + "recorded_at", + "steps" + ], + "properties": { + "entry": { + "$ref": "#/definitions/TraceEntry" + }, + "pages": { + "type": "array", + "items": { + "$ref": "#/definitions/PageRefV2" + } + }, + "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" + } + } + }, + "additionalProperties": false + }, + "TraceV3": { + "description": "Wire trace returned by `tool.record_stop` / `await`.", "type": "object", "required": [ "entry", @@ -829,13 +914,13 @@ "states": { "type": "array", "items": { - "$ref": "#/definitions/TraceState" + "$ref": "#/definitions/TraceStateV3" } }, "steps": { "type": "array", "items": { - "$ref": "#/definitions/Step" + "$ref": "#/definitions/StepV3" } }, "stopped_by": { @@ -847,101 +932,6 @@ } }, "additionalProperties": false - }, - "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", - "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" - } - } - }, - "additionalProperties": false } } } diff --git a/crates/bsk-protocol/schema/trace.json b/crates/bsk-protocol/schema/trace.json index 2abfd55..750ce6a 100644 --- a/crates/bsk-protocol/schema/trace.json +++ b/crates/bsk-protocol/schema/trace.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Trace", - "description": "Persisted user-action trace exported by `tool.record_stop` / `await`.", + "title": "TraceV3", + "description": "Wire trace returned by `tool.record_stop` / `await`.", "type": "object", "required": [ "entry", @@ -37,13 +37,13 @@ "states": { "type": "array", "items": { - "$ref": "#/definitions/TraceState" + "$ref": "#/definitions/TraceStateV3" } }, "steps": { "type": "array", "items": { - "$ref": "#/definitions/Step" + "$ref": "#/definitions/StepV3" } }, "stopped_by": { @@ -103,7 +103,7 @@ } } }, - "SelectedOption": { + "SelectedOptionV3": { "description": "One selected option (`select` op).", "type": "object", "required": [ @@ -121,7 +121,18 @@ } } }, - "Step": { + "StepResultV3": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string" + } + } + }, + "StepV3": { "description": "One recorded user action — discriminated union by `op`.", "oneOf": [ { @@ -151,7 +162,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -185,14 +196,14 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -219,14 +230,14 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -261,14 +272,14 @@ "type": "boolean" }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" }, "value": { "type": "string" @@ -298,12 +309,12 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOption" + "$ref": "#/definitions/SelectedOptionV3" } }, "state": { @@ -311,7 +322,7 @@ "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -350,7 +361,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -359,7 +370,7 @@ "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" }, { "type": "null" @@ -390,7 +401,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -400,17 +411,6 @@ } ] }, - "StepResult": { - "type": "object", - "required": [ - "state" - ], - "properties": { - "state": { - "type": "string" - } - } - }, "StopReason": { "type": "string", "enum": [ @@ -418,7 +418,7 @@ "cli_stop" ] }, - "TargetDescriptor": { + "TargetDescriptorV3": { "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", "properties": { @@ -452,7 +452,6 @@ } }, "TraceEntry": { - "description": "Recording entry point — first URL the flow starts from.", "type": "object", "required": [ "start_url" @@ -463,31 +462,22 @@ } } }, - "TraceState": { + "TraceStateV3": { "description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.", "type": "object", "required": [ + "body", "id", "url" ], "properties": { "body": { - "description": "Wire-only: full page observation (front matter + VOM body + annotations).", - "type": [ - "string", - "null" - ] + "description": "Full page observation (front matter + VOM body + annotations).", + "type": "string" }, "id": { "type": "string" }, - "page": { - "description": "Disk-only: filename under the bundle `pages/` directory.", - "type": [ - "string", - "null" - ] - }, "title": { "type": [ "string", diff --git a/crates/bsk-protocol/schema/trace_step.json b/crates/bsk-protocol/schema/trace_step.json index 79f5e1e..14a33ee 100644 --- a/crates/bsk-protocol/schema/trace_step.json +++ b/crates/bsk-protocol/schema/trace_step.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Step", + "title": "StepV3", "description": "One recorded user action — discriminated union by `op`.", "oneOf": [ { @@ -30,7 +30,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -64,14 +64,14 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -98,14 +98,14 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -140,14 +140,14 @@ "type": "boolean" }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" }, "value": { "type": "string" @@ -177,12 +177,12 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOption" + "$ref": "#/definitions/SelectedOptionV3" } }, "state": { @@ -190,7 +190,7 @@ "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -229,7 +229,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -238,7 +238,7 @@ "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" }, { "type": "null" @@ -269,7 +269,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -309,7 +309,7 @@ "browser" ] }, - "SelectedOption": { + "SelectedOptionV3": { "description": "One selected option (`select` op).", "type": "object", "required": [ @@ -327,7 +327,7 @@ } } }, - "StepResult": { + "StepResultV3": { "type": "object", "required": [ "state" @@ -338,7 +338,7 @@ } } }, - "TargetDescriptor": { + "TargetDescriptorV3": { "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", "properties": { diff --git a/crates/bsk-protocol/schema/trace_v2.json b/crates/bsk-protocol/schema/trace_v2.json index c301ee9..7db328c 100644 --- a/crates/bsk-protocol/schema/trace_v2.json +++ b/crates/bsk-protocol/schema/trace_v2.json @@ -16,7 +16,7 @@ "pages": { "type": "array", "items": { - "$ref": "#/definitions/PageRef" + "$ref": "#/definitions/PageRefV2" } }, "purpose": { @@ -54,7 +54,7 @@ "shift" ] }, - "PageRef": { + "PageRefV2": { "description": "Page context dictionary entry — referenced by steps via `page` id.", "type": "object", "required": [ @@ -431,7 +431,6 @@ } }, "TraceEntry": { - "description": "Recording entry point — first URL the flow starts from.", "type": "object", "required": [ "start_url" diff --git a/crates/bsk-protocol/src/bin/dump-schema.rs b/crates/bsk-protocol/src/bin/dump-schema.rs index 2071a05..6e565fe 100644 --- a/crates/bsk-protocol/src/bin/dump-schema.rs +++ b/crates/bsk-protocol/src/bin/dump-schema.rs @@ -114,8 +114,8 @@ fn main() { dump!(RequestHelpResult, "tool_request_help_result"); dump!(TraceV2, "trace_v2"); - dump!(Trace, "trace"); - dump!(Step, "trace_step"); + dump!(TraceV3, "trace"); + dump!(StepV3, "trace_step"); dump!(RecordStartParams, "tool_record_start_params"); dump!(RecordStartResult, "tool_record_start_result"); dump!(RecordStopParams, "tool_record_stop_params"); diff --git a/crates/bsk-protocol/src/tools/mod.rs b/crates/bsk-protocol/src/tools/mod.rs index 67e8f53..11ab651 100644 --- a/crates/bsk-protocol/src/tools/mod.rs +++ b/crates/bsk-protocol/src/tools/mod.rs @@ -9,7 +9,8 @@ pub mod navigation; pub mod network; pub mod observation; pub mod record; -pub mod record_v2; +mod record_common; +mod record_v2; pub mod script; pub mod session; pub mod tabs; diff --git a/crates/bsk-protocol/src/tools/record.rs b/crates/bsk-protocol/src/tools/record.rs index 8111965..d4e862b 100644 --- a/crates/bsk-protocol/src/tools/record.rs +++ b/crates/bsk-protocol/src/tools/record.rs @@ -8,11 +8,12 @@ use serde::{Deserialize, Deserializer, Serialize}; use super::interaction::KeyModifier; -pub use crate::record_v2::{ - PageRef, SelectedOptionV2, StepCommonV2, StepEffectV2, StepV2, TargetDescriptorV2, TraceV2, +pub use super::record_common::TraceEntry; +pub use super::record_v2::{ + PageRefV2, SelectedOptionV2, StepCommonV2, StepEffectV2, StepV2, TargetDescriptorV2, TraceV2, }; -pub const TRACE_VERSION: u32 = 3; +pub const TRACE_VERSION_V3: u32 = 3; pub const TRACE_VERSION_V2: u32 = 2; pub const DEFAULT_TRACE_VERSION: u32 = 2; pub const VOM_FORMAT_VERSION: u32 = 1; @@ -22,9 +23,9 @@ where D: Deserializer<'de>, { let version = u32::deserialize(deserializer)?; - if version != TRACE_VERSION { + if version != TRACE_VERSION_V3 { return Err(serde::de::Error::custom(format!( - "unsupported trace version {version} (expected {TRACE_VERSION})" + "unsupported trace version {version} (expected {TRACE_VERSION_V3})" ))); } Ok(version) @@ -33,7 +34,7 @@ where fn trace_v3_version_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { schemars::schema::SchemaObject { instance_type: Some(schemars::schema::InstanceType::Integer.into()), - const_value: Some(serde_json::json!(TRACE_VERSION)), + const_value: Some(serde_json::json!(TRACE_VERSION_V3)), ..Default::default() } .into() @@ -45,7 +46,7 @@ fn trace_v3_version_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars /// Stable semantic handle for an interacted element within a page observation. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct TargetDescriptor { +pub struct TargetDescriptorV3 { #[serde(default, skip_serializing_if = "Option::is_none", rename = "ref")] pub element_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -59,15 +60,9 @@ pub struct TargetDescriptor { } // --------------------------------------------------------------------------- -// Trace envelope +// Trace v3 envelope // --------------------------------------------------------------------------- -/// Recording entry point — first URL the flow starts from. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct TraceEntry { - pub start_url: String, -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct RecorderInfo { pub bsk: String, @@ -83,42 +78,38 @@ pub enum StopReason { /// Page observation dictionary entry — referenced by steps via `state` / `result.state`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct TraceState { +pub struct TraceStateV3 { pub id: String, pub url: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, - /// Wire-only: full page observation (front matter + VOM body + annotations). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub body: Option, - /// Disk-only: filename under the bundle `pages/` directory. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub page: Option, + /// Full page observation (front matter + VOM body + annotations). + pub body: String, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub truncated: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct StepResult { +pub struct StepResultV3 { pub state: String, } /// Fields shared by every step variant (flattened in JSON). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct StepCommon { +pub struct StepCommonV3 { pub id: u32, /// Observation id immediately before this action. pub state: String, - pub result: StepResult, + pub result: StepResultV3, } // --------------------------------------------------------------------------- -// Step op-specific payloads +// Trace v3 step payloads // --------------------------------------------------------------------------- /// One selected option (`select` op). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct SelectedOption { +pub struct SelectedOptionV3 { pub value: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub label: Option, @@ -147,27 +138,27 @@ pub enum FillCommit { /// One recorded user action — discriminated union by `op`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "op", rename_all = "snake_case")] -pub enum Step { +pub enum StepV3 { Navigate { #[serde(flatten)] - common: StepCommon, + common: StepCommonV3, to: String, cause: NavigationCause, }, Click { #[serde(flatten)] - common: StepCommon, - target: TargetDescriptor, + common: StepCommonV3, + target: TargetDescriptorV3, }, Hover { #[serde(flatten)] - common: StepCommon, - target: TargetDescriptor, + common: StepCommonV3, + target: TargetDescriptorV3, }, Fill { #[serde(flatten)] - common: StepCommon, - target: TargetDescriptor, + common: StepCommonV3, + target: TargetDescriptorV3, value: String, commit: FillCommit, #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -175,34 +166,34 @@ pub enum Step { }, Select { #[serde(flatten)] - common: StepCommon, - target: TargetDescriptor, + common: StepCommonV3, + target: TargetDescriptorV3, #[serde(default, skip_serializing_if = "Vec::is_empty")] - selection: Vec, + selection: Vec, }, Press { #[serde(flatten)] - common: StepCommon, + common: StepCommonV3, key: String, #[serde(default, skip_serializing_if = "Option::is_none")] modifiers: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - target: Option, + target: Option, }, Scroll { #[serde(flatten)] - common: StepCommon, + common: StepCommonV3, }, } // --------------------------------------------------------------------------- -// Trace root +// Trace v3 root // --------------------------------------------------------------------------- -/// Persisted user-action trace exported by `tool.record_stop` / `await`. +/// Wire trace returned by `tool.record_stop` / `await`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] -pub struct Trace { +pub struct TraceV3 { #[serde(deserialize_with = "deserialize_trace_v3_version")] #[schemars(schema_with = "trace_v3_version_schema")] pub version: u32, @@ -214,8 +205,8 @@ pub struct Trace { pub stopped_by: StopReason, pub entry: TraceEntry, pub recorder: RecorderInfo, - pub states: Vec, - pub steps: Vec, + pub states: Vec, + pub steps: Vec, } // --------------------------------------------------------------------------- @@ -231,13 +222,6 @@ pub struct RecordStartParams { pub url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub purpose: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_page_tokens: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub redact_values: Option, - /// Desired trace export format. Omitted ⇒ v2; `3` ⇒ state-linked v3 bundle. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub trace_version: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -255,13 +239,13 @@ pub struct RecordStopParams { #[derive(Debug, Clone, PartialEq)] pub enum RecordedTrace { V2(TraceV2), - V3(Trace), + V3(TraceV3), } impl RecordedTrace { pub fn classify_value(v: &serde_json::Value) -> Result { if let Some(ver) = v.get("version").and_then(|x| x.as_u64()) { - if ver != u64::from(TRACE_VERSION) { + if ver != u64::from(TRACE_VERSION_V3) { return Err(format!("unsupported trace version {ver}")); } if v.get("pages").is_some() { @@ -281,24 +265,6 @@ impl RecordedTrace { } 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 { @@ -333,7 +299,7 @@ impl JsonSchema for RecordedTrace { subschemas: Some(Box::new(schemars::schema::SubschemaValidation { one_of: Some(vec![ generator.subschema_for::(), - generator.subschema_for::(), + generator.subschema_for::(), ]), ..Default::default() })), @@ -369,18 +335,18 @@ mod tests { use super::*; use serde_json::json; - fn sample_common(id: u32, state: &str, result_state: &str) -> StepCommon { - StepCommon { + fn sample_common(id: u32, state: &str, result_state: &str) -> StepCommonV3 { + StepCommonV3 { id, state: state.into(), - result: StepResult { + result: StepResultV3 { state: result_state.into(), }, } } - fn sample_target() -> TargetDescriptor { - TargetDescriptor { + fn sample_target() -> TargetDescriptorV3 { + TargetDescriptorV3 { element_ref: Some("e21".into()), role: Some("button".into()), name: Some("发布".into()), @@ -389,9 +355,9 @@ mod tests { } } - fn sample_trace() -> Trace { - Trace { - version: TRACE_VERSION, + fn sample_trace() -> TraceV3 { + TraceV3 { + version: TRACE_VERSION_V3, purpose: Some("把草稿商品发布上架".into()), started_at: Some("2026-08-10T02:10:41.080Z".into()), recorded_at: "2026-08-10T02:12:55.360Z".into(), @@ -404,32 +370,30 @@ mod tests { vom: VOM_FORMAT_VERSION, }, states: vec![ - TraceState { + TraceStateV3 { id: "s1".into(), url: "https://example.com/".into(), title: Some("Example Domain".into()), - body: None, - page: Some("s1.vom.txt".into()), + body: "@vom 1\nRootWebArea \"Example Domain\"".into(), truncated: false, }, - TraceState { + TraceStateV3 { id: "s2".into(), url: "https://shop.example.com/products?status=draft".into(), title: Some("商品管理".into()), - body: None, - page: Some("s2.vom.txt".into()), + body: "@vom 1\nRootWebArea \"商品管理\"".into(), truncated: false, }, ], steps: vec![ - Step::Navigate { + StepV3::Navigate { common: sample_common(1, "s1", "s2"), to: "https://shop.example.com/products?status=draft".into(), cause: NavigationCause::UserTyped, }, - Step::Fill { + StepV3::Fill { common: sample_common(2, "s2", "s2"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: Some("e12".into()), role: Some("textbox".into()), name: Some("搜索商品".into()), @@ -440,7 +404,7 @@ mod tests { commit: FillCommit::Enter, redacted: false, }, - Step::Click { + StepV3::Click { common: sample_common(3, "s2", "s2"), target: sample_target(), }, @@ -450,7 +414,7 @@ mod tests { #[test] fn step_click_round_trips() { - let step = Step::Click { + let step = StepV3::Click { common: sample_common(1, "s1", "s2"), target: sample_target(), }; @@ -459,15 +423,15 @@ mod tests { 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(); + let round: StepV3 = serde_json::from_value(v).unwrap(); assert_eq!(round, step); } #[test] fn step_fill_with_commit_round_trips() { - let step = Step::Fill { + let step = StepV3::Fill { common: sample_common(2, "s2", "s3"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: Some("e12".into()), role: Some("textbox".into()), name: Some("搜索商品".into()), @@ -482,15 +446,15 @@ mod tests { assert_eq!(v["op"], "fill"); assert_eq!(v["commit"], "enter"); assert!(v.get("redacted").is_none()); - let round: Step = serde_json::from_value(v).unwrap(); + let round: StepV3 = serde_json::from_value(v).unwrap(); assert_eq!(round, step); } #[test] fn step_fill_password_is_redacted() { - let step = Step::Fill { + let step = StepV3::Fill { common: sample_common(1, "s1", "s1"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: Some("e3".into()), role: Some("textbox".into()), name: Some("密码".into()), @@ -508,7 +472,7 @@ mod tests { #[test] fn step_navigate_with_cause_round_trips() { - let step = Step::Navigate { + let step = StepV3::Navigate { common: sample_common(1, "s1", "s2"), to: "https://example.com".into(), cause: NavigationCause::UserTyped, @@ -516,7 +480,7 @@ mod tests { let v = serde_json::to_value(&step).unwrap(); assert_eq!(v["op"], "navigate"); assert_eq!(v["cause"], "user_typed"); - let round: Step = serde_json::from_value(v).unwrap(); + let round: StepV3 = serde_json::from_value(v).unwrap(); assert_eq!(round, step); } @@ -528,15 +492,15 @@ mod tests { 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(); + let round: TraceV3 = serde_json::from_value(v).unwrap(); assert_eq!(round, trace); } #[test] fn default_fields_are_omitted() { - let step = Step::Click { + let step = StepV3::Click { common: sample_common(1, "s1", "s1"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: Some("e1".into()), role: Some("button".into()), name: Some("OK".into()), @@ -552,9 +516,9 @@ mod tests { #[test] fn unmatched_target_serializes_flag() { - let step = Step::Click { + let step = StepV3::Click { common: sample_common(1, "s1", "s2"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: None, role: Some("button".into()), name: Some("发布".into()), @@ -628,7 +592,7 @@ mod tests { "steps": [] }); assert!(RecordedTrace::classify_value(&unsupported).is_err()); - assert!(serde_json::from_value::(unsupported).is_err()); + assert!(serde_json::from_value::(unsupported).is_err()); } #[test] @@ -639,7 +603,7 @@ mod tests { .expect("RecordedTrace schema should use oneOf"); assert_eq!(variants.len(), 2); - let trace_schema = schema["definitions"]["Trace"].clone(); + let trace_schema = schema["definitions"]["TraceV3"].clone(); assert_eq!(trace_schema["properties"]["version"]["const"], 3); } @@ -654,8 +618,8 @@ mod tests { "entry": { "start_url": "https://example.com/editor" }, "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" } + { "id": "s1", "url": "https://example.com/editor", "body": "@vom 1" }, + { "id": "s2", "url": "https://example.com/p/99", "body": "@vom 1" } ], "steps": [ { @@ -676,7 +640,7 @@ mod tests { } ] }); - let trace: Trace = serde_json::from_value(v).unwrap(); + let trace: TraceV3 = serde_json::from_value(v).unwrap(); assert_eq!(trace.version, 3); assert_eq!(trace.states.len(), 2); assert_eq!(trace.steps.len(), 2); diff --git a/crates/bsk-protocol/src/tools/record_common.rs b/crates/bsk-protocol/src/tools/record_common.rs new file mode 100644 index 0000000..0261736 --- /dev/null +++ b/crates/bsk-protocol/src/tools/record_common.rs @@ -0,0 +1,7 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct TraceEntry { + pub start_url: String, +} diff --git a/crates/bsk-protocol/src/tools/record_v2.rs b/crates/bsk-protocol/src/tools/record_v2.rs index 27f7f3f..43b36e7 100644 --- a/crates/bsk-protocol/src/tools/record_v2.rs +++ b/crates/bsk-protocol/src/tools/record_v2.rs @@ -1,13 +1,12 @@ //! 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. +//! Legacy wire format with no top-level `version` field. use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::interaction::KeyModifier; -use super::record::TraceEntry; +use super::record_common::TraceEntry; /// Stable semantic handle for an interacted element (v2). /// @@ -29,7 +28,7 @@ pub struct TargetDescriptorV2 { /// Page context dictionary entry — referenced by steps via `page` id. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct PageRef { +pub struct PageRefV2 { pub id: String, pub url: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -116,7 +115,7 @@ pub struct TraceV2 { #[serde(default, skip_serializing_if = "Option::is_none")] pub purpose: Option, pub entry: TraceEntry, - pub pages: Vec, + pub pages: Vec, pub steps: Vec, } @@ -153,7 +152,7 @@ mod tests { entry: TraceEntry { start_url: "https://example.com/".into(), }, - pages: vec![PageRef { + pages: vec![PageRefV2 { id: "p1".into(), url: "https://example.com/".into(), title: None,