Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ lib/v3/dom/build/
packages/core/dist/
packages/core/lib/dom/build/
packages/core/lib/v3/dom/build/
packages/core/gen/
packages/evals/dist/
packages/docs/
*.min.js
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default [
"**/node_modules/**",
"packages/core/lib/dom/build/**",
"packages/core/lib/v3/dom/build/**",
"packages/core/gen/**",
"**/*.config.js",
"**/*.config.mjs",
],
Expand Down
6 changes: 6 additions & 0 deletions packages/core/buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: v2
plugins:
- local: protoc-gen-es
out: gen
include_imports: true
opt: target=ts
6 changes: 6 additions & 0 deletions packages/core/buf.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Generated by buf. DO NOT EDIT.
version: v2
deps:
- name: buf.build/bufbuild/protovalidate
commit: 52f32327d4b045a79293a6ad4e7e1236
digest: b5:cbabc98d4b7b7b0447c9b15f68eeb8a7a44ef8516cb386ac5f66e7fd4062cd6723ed3f452ad8c384b851f79e33d26e7f8a94e2b807282b3def1cd966c7eace97
11 changes: 11 additions & 0 deletions packages/core/buf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: v2
modules:
- path: proto
deps:
- buf.build/bufbuild/protovalidate
lint:
use:
- STANDARD
breaking:
use:
- FILE
4,963 changes: 4,963 additions & 0 deletions packages/core/gen/buf/validate/validate_pb.ts

Large diffs are not rendered by default.

88 changes: 88 additions & 0 deletions packages/core/gen/stagehand/v1/ping_pb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated from file stagehand/v1/ping.proto (package stagehand.v1, syntax proto3)
/* eslint-disable */

import type {
GenFile,
GenMessage,
GenService,
} from "@bufbuild/protobuf/codegenv2";
import {
fileDesc,
messageDesc,
serviceDesc,
} from "@bufbuild/protobuf/codegenv2";
import type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf";

/**
* Describes the file stagehand/v1/ping.proto.
*/
export const file_stagehand_v1_ping: GenFile =
/*@__PURE__*/
fileDesc(
"ChdzdGFnZWhhbmQvdjEvcGluZy5wcm90bxIMc3RhZ2VoYW5kLnYxIkMKC1BpbmdSZXF1ZXN0EjQKEGNsaWVudF9zZW5kX3RpbWUYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInoKDFBpbmdSZXNwb25zZRI0ChBjbGllbnRfc2VuZF90aW1lGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBI0ChBzZXJ2ZXJfc2VuZF90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcDJVChRTdGFnZWhhbmRQaW5nU2VydmljZRI9CgRQaW5nEhkuc3RhZ2VoYW5kLnYxLlBpbmdSZXF1ZXN0Ghouc3RhZ2VoYW5kLnYxLlBpbmdSZXNwb25zZWIGcHJvdG8z",
[file_google_protobuf_timestamp],
);

/**
* @generated from message stagehand.v1.PingRequest
*/
export type PingRequest = Message<"stagehand.v1.PingRequest"> & {
/**
* Timestamp representing when the client emitted the ping.
*
* @generated from field: google.protobuf.Timestamp client_send_time = 1;
*/
clientSendTime?: Timestamp;
};

/**
* Describes the message stagehand.v1.PingRequest.
* Use `create(PingRequestSchema)` to create a new message.
*/
export const PingRequestSchema: GenMessage<PingRequest> =
/*@__PURE__*/
messageDesc(file_stagehand_v1_ping, 0);

/**
* @generated from message stagehand.v1.PingResponse
*/
export type PingResponse = Message<"stagehand.v1.PingResponse"> & {
/**
* Echo of the client's send time so latency can be derived from RTT.
*
* @generated from field: google.protobuf.Timestamp client_send_time = 1;
*/
clientSendTime?: Timestamp;

/**
* Timestamp representing when the server crafted the response.
*
* @generated from field: google.protobuf.Timestamp server_send_time = 2;
*/
serverSendTime?: Timestamp;
};

/**
* Describes the message stagehand.v1.PingResponse.
* Use `create(PingResponseSchema)` to create a new message.
*/
export const PingResponseSchema: GenMessage<PingResponse> =
/*@__PURE__*/
messageDesc(file_stagehand_v1_ping, 1);

/**
* @generated from service stagehand.v1.StagehandPingService
*/
export const StagehandPingService: GenService<{
/**
* @generated from rpc stagehand.v1.StagehandPingService.Ping
*/
ping: {
methodKind: "unary";
input: typeof PingRequestSchema;
output: typeof PingResponseSchema;
};
}> = /*@__PURE__*/ serviceDesc(file_stagehand_v1_ping, 0);
16 changes: 15 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@
"typecheck": "tsc --noEmit",
"prepare": "pnpm run build",
"build": "pnpm run gen-version && pnpm run build-dom-scripts && pnpm run build-js && pnpm run typecheck",
"dev": "tsx server/server.ts",
"client": "tsx server/client.ts",
"example": "node --import tsx -e \"const args=process.argv.slice(1).filter(a=>a!=='--'); const [p]=args; const n=(p||'example').replace(/^\\.\\//,'').replace(/\\.ts$/i,''); import(new URL(require('node:path').resolve('examples', n + '.ts'), 'file:'));\" --",
"test": "playwright test --config=lib/v3/tests/v3.playwright.config.ts",
"e2e": "playwright test --config=lib/v3/tests/v3.local.playwright.config.ts",
"e2e:local": "playwright test --config=lib/v3/tests/v3.local.playwright.config.ts",
"e2e:bb": "playwright test --config=lib/v3/tests/v3.bb.playwright.config.ts",
"lint": "cd ../.. && prettier --check packages/core && cd packages/core && eslint .",
"format": "prettier --write .",
"test:vitest": "pnpm run build-js && pnpm run typecheck && vitest run --config vitest.config.ts"
"test:vitest": "pnpm run build-js && pnpm run typecheck && vitest run --config vitest.config.ts",
"rpc:deps": "pnpm exec buf dep update",
"rpc:lint": "pnpm exec buf lint",
"rpc:generate": "pnpm exec buf generate"
},
"files": [
"dist/index.js",
Expand All @@ -46,11 +51,18 @@
"@ai-sdk/provider": "^2.0.0",
"@anthropic-ai/sdk": "0.39.0",
"@browserbasehq/sdk": "^2.4.0",
"@bufbuild/protobuf": "^2.10.1",
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-fastify": "^2.1.1",
"@connectrpc/connect-node": "^2.1.1",
"@connectrpc/validate": "^0.2.0",
"@fastify/cors": "^10.0.1",
"@google/genai": "^1.22.0",
"@langchain/openai": "^0.4.4",
"@modelcontextprotocol/sdk": "^1.17.2",
"ai": "^5.0.0",
"devtools-protocol": "^0.0.1464554",
"fastify": "^5.2.4",
"fetch-cookie": "^3.1.0",
"openai": "^4.87.1",
"pino": "^9.6.0",
Expand Down Expand Up @@ -80,6 +92,8 @@
"puppeteer-core": "^22.8.0"
},
"devDependencies": {
"@bufbuild/buf": "^1.61.0",
"@bufbuild/protoc-gen-es": "^2.10.1",
"@playwright/test": "^1.42.1",
"eslint": "^9.16.0",
"prettier": "^3.2.5",
Expand Down
22 changes: 22 additions & 0 deletions packages/core/proto/stagehand/v1/ping.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
syntax = "proto3";

package stagehand.v1;

import "google/protobuf/timestamp.proto";

message PingRequest {
// Timestamp representing when the client emitted the ping.
google.protobuf.Timestamp client_send_time = 1;
}

message PingResponse {
// Echo of the client's send time so latency can be derived from RTT.
google.protobuf.Timestamp client_send_time = 1;

// Timestamp representing when the server crafted the response.
google.protobuf.Timestamp server_send_time = 2;
}

service StagehandPingService {
rpc Ping(PingRequest) returns (PingResponse);
}
55 changes: 55 additions & 0 deletions packages/core/server/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-node";
import { timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
import { StagehandPingService } from "../gen/stagehand/v1/ping_pb";
import {
pingRequestSchema,
pingResponseSchema,
timestampFromSecondsAndNanos,
} from "./schema/ping";

async function main() {
const transport = createConnectTransport({
baseUrl: "http://localhost:8080",
httpVersion: "1.1",
});

const client = createClient(StagehandPingService, transport);

const t0 = Date.now();
const parsedRequest = pingRequestSchema.parse({
clientSendTime: timestampFromDate(new Date(t0)),
});
// Convert Zod-validated plain object back to Timestamp Message for gRPC client
const pingRequest = {
clientSendTime: timestampFromSecondsAndNanos(parsedRequest.clientSendTime),
};
const rawResponse = await client.ping(pingRequest);
const pingResponse = pingResponseSchema.parse(rawResponse);

const t3 = Date.now();
const rtt = t3 - t0;
const latency = rtt / 2;
const clientSendTimeMs = timestampDate(pingResponse.clientSendTime).getTime();
const serverSendTimeMs = timestampDate(pingResponse.serverSendTime).getTime();
const offset = serverSendTimeMs - (t0 + latency);

console.log(
JSON.stringify(
{
clientSendTime: clientSendTimeMs,
serverSendTime: serverSendTimeMs,
rtt,
latency,
offset,
},
null,
2,
),
);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
22 changes: 22 additions & 0 deletions packages/core/server/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ConnectError, Code, type ConnectRouter } from "@connectrpc/connect";
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
import { StagehandPingService } from "../gen/stagehand/v1/ping_pb";
import { pingRequestSchema } from "./schema/ping";

export default (router: ConnectRouter) =>
router.service(StagehandPingService, {
async ping(req) {
const parsedReq = pingRequestSchema.safeParse(req);
if (!parsedReq.success) {
throw new ConnectError(
`Invalid PingRequest: ${parsedReq.error.message}`,
Code.InvalidArgument,
);
}
const serverSendTime = timestampFromDate(new Date());
return {
clientSendTime: parsedReq.data.clientSendTime,
serverSendTime,
};
},
});
54 changes: 54 additions & 0 deletions packages/core/server/schema/ping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { timestampFromDate, type Timestamp } from "@bufbuild/protobuf/wkt";
import { z } from "zod";

const timestampShape = z.object({
seconds: z.bigint({ message: "timestamp seconds required" }),
nanos: z
.number({ message: "timestamp nanos required" })
.int({ message: "timestamp nanos must be an integer" })
.gte(0, { message: "timestamp nanos must be >= 0" })
.lte(999_999_999, { message: "timestamp nanos must be < 1,000,000,000" }),
});

const preprocessTimestamp = (value: unknown) => {
if (value instanceof Date) {
return timestampFromDate(value);
}
if (typeof value === "number") {
return timestampFromDate(new Date(value));
}
return value;
};

const timestampSchema = z
.preprocess(preprocessTimestamp, timestampShape)
.refine(
(value) =>
value.seconds > BigInt(0) ||
(value.seconds === BigInt(0) && value.nanos > 0),
{
message: "timestamp must be greater than zero milliseconds",
},
);

/**
* Converts a plain object with seconds and nanos back to a Timestamp Message.
* This is needed because Zod validation strips the Message type metadata.
*/
export function timestampFromSecondsAndNanos(value: {
seconds: bigint;
nanos: number;
}): Timestamp {
// Convert seconds (Unix timestamp) to milliseconds for Date constructor
const date = new Date(Number(value.seconds) * 1000 + value.nanos / 1_000_000);
return timestampFromDate(date);
}

export const pingRequestSchema = z.object({
clientSendTime: timestampSchema,
});

export const pingResponseSchema = z.object({
clientSendTime: timestampSchema,
serverSendTime: timestampSchema,
});
20 changes: 20 additions & 0 deletions packages/core/server/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createValidateInterceptor } from "@connectrpc/validate";
import { fastify } from "fastify";
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
import routes from "./connect";

async function main() {
const server = fastify();
await server.register(fastifyConnectPlugin, {
interceptors: [createValidateInterceptor()],
routes,
});
server.get("/", (_, reply) => {
reply.type("text/plain");
reply.send("Hello World!");
});
await server.listen({ host: "localhost", port: 8080 });
console.log("server is listening at", server.addresses());
}

void main();
Loading