Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
60 changes: 60 additions & 0 deletions packages/core/examples/flowLoggingJourney.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Stagehand } from "../lib/v3";

async function run(): Promise<void> {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error(
"Set OPENAI_API_KEY to a valid OpenAI key before running this demo.",
);
}

const stagehand = new Stagehand({
env: "LOCAL",
verbose: 2,
model: { modelName: "openai/gpt-4.1-mini", apiKey },
localBrowserLaunchOptions: {
headless: true,
args: ["--window-size=1280,720"],
},
disablePino: true,
});

try {
await stagehand.init();

const [page] = stagehand.context.pages();
await page.goto("https://example.com/", { waitUntil: "load" });

const agent = stagehand.agent({
systemPrompt:
"You are a QA assistant. Keep answers short and deterministic. Finish quickly.",
});
const agentResult = await agent.execute(
"Glance at the Example Domain page and confirm that you see the hero text.",
);
console.log("Agent result:", agentResult);

const observations = await stagehand.observe(
"Locate the 'More information...' link on this page.",
);
console.log("Observe result:", observations);

if (observations.length > 0) {
await stagehand.act(observations[0]);
} else {
await stagehand.act("click the link labeled 'More information...'");
}

const extraction = await stagehand.extract(
"Summarize the current page title and URL.",
);
console.log("Extraction result:", extraction);
} finally {
await stagehand.close({ force: true }).catch(() => {});
}
}

run().catch((error) => {
console.error(error);
process.exitCode = 1;
});
271 changes: 271 additions & 0 deletions packages/core/lib/v3/flowLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { randomUUID } from "node:crypto";
import { v3Logger } from "./logger";

type FlowPrefixOptions = {
includeAction?: boolean;
includeStep?: boolean;
includeTask?: boolean;
};

const MAX_ARG_LENGTH = 500;

let currentTaskId: string | null = null;
let currentStepId: string | null = null;
let currentActionId: string | null = null;
let currentStepLabel: string | null = null;
let currentActionLabel: string | null = null;

function generateId(label: string): string {
try {
return randomUUID();
} catch {
const fallback =
(globalThis.crypto as Crypto | undefined)?.randomUUID?.() ??
`${Date.now()}-${label}-${Math.floor(Math.random() * 1e6)}`;
return fallback;
}
}

function truncate(value: string): string {
if (value.length <= MAX_ARG_LENGTH) {
return value;
}
return `${value.slice(0, MAX_ARG_LENGTH)}…`;
}

function formatValue(value: unknown): string {
if (typeof value === "string") {
return `'${value}'`;
}
if (
typeof value === "number" ||
typeof value === "boolean" ||
value === null
) {
return String(value);
}
if (Array.isArray(value)) {
try {
return truncate(JSON.stringify(value));
} catch {
return "[unserializable array]";
}
}
if (typeof value === "object" && value !== null) {
try {
return truncate(JSON.stringify(value));
} catch {
return "[unserializable object]";
}
}
if (value === undefined) {
return "undefined";
}
return truncate(String(value));
}

function formatArgs(args?: unknown | unknown[]): string {
if (args === undefined) {
return "";
}
const normalized = (Array.isArray(args) ? args : [args]).filter(
(entry) => entry !== undefined,
);
const rendered = normalized
.map((entry) => formatValue(entry))
.filter((entry) => entry.length > 0);
return rendered.join(", ");
}

function formatTag(label: string, id: string | null): string {
return `[${label} #${shortId(id)}]`;
}

function formatCdpTag(sessionId?: string | null): string {
if (!sessionId) return "[CDP]";
return `[CDP #${shortId(sessionId).toUpperCase()}]`;
}

function shortId(id: string | null): string {
if (!id) return "-";
const trimmed = id.slice(-4);
return trimmed;
}

function ensureTaskContext(): void {
if (!currentTaskId) {
currentTaskId = generateId("task");
}
}

function ensureStepContext(defaultLabel?: string): void {
if (defaultLabel) {
currentStepLabel = defaultLabel.toUpperCase();
}
if (!currentStepLabel) {
currentStepLabel = "STEP";
}
if (!currentStepId) {
currentStepId = generateId("step");
}
}

function ensureActionContext(defaultLabel?: string): void {
if (defaultLabel) {
currentActionLabel = defaultLabel.toUpperCase();
}
if (!currentActionLabel) {
currentActionLabel = "ACTION";
}
if (!currentActionId) {
currentActionId = generateId("action");
}
}

function buildPrefix({
includeAction = true,
includeStep = true,
includeTask = true,
}: FlowPrefixOptions = {}): string {
const parts: string[] = [];
if (includeTask) {
ensureTaskContext();
parts.push(formatTag("TASK", currentTaskId));
}
if (includeStep) {
ensureStepContext();
const label = currentStepLabel ?? "STEP";
parts.push(formatTag(label, currentStepId));
}
if (includeAction) {
ensureActionContext();
const actionLabel = currentActionLabel ?? "ACTION";
parts.push(formatTag(actionLabel, currentActionId));
}
return parts.join(" ");
}

export function logTaskProgress({
invocation,
args,
}: {
invocation: string;
args?: unknown | unknown[];
}): string {
currentTaskId = generateId("task");
currentStepId = null;
currentActionId = null;
currentStepLabel = null;
currentActionLabel = null;

const call = `${invocation}(${formatArgs(args)})`;
const message = `${buildPrefix({
includeTask: true,
includeStep: false,
includeAction: false,
})} ${call}`;
v3Logger({
category: "flow",
message,
level: 2,
});
return currentTaskId;
}

export function logStepProgress({
invocation,
args,
label,
}: {
invocation: string;
args?: unknown | unknown[];
label: string;
}): string {
ensureTaskContext();
currentStepId = generateId("step");
currentStepLabel = label.toUpperCase();
currentActionId = null;
currentActionLabel = null;

const call = `${invocation}(${formatArgs(args)})`;
const message = `${buildPrefix({
includeTask: true,
includeStep: true,
includeAction: false,
})} ${call}`;
v3Logger({
category: "flow",
message,
level: 2,
});
return currentStepId;
}

export function logActionProgress({
actionType,
target,
args,
}: {
actionType: string;
target?: string;
args?: unknown | unknown[];
}): string {
ensureTaskContext();
ensureStepContext();
currentActionId = generateId("action");
currentActionLabel = actionType.toUpperCase();
const details: string[] = [`${actionType}`];
if (target) {
details.push(`target=${target}`);
}
const argString = formatArgs(args);
if (argString) {
details.push(`args=[${argString}]`);
}

const message = `${buildPrefix({
includeTask: true,
includeStep: true,
includeAction: true,
})} ${details.join(" ")}`;
v3Logger({
category: "flow",
message,
level: 2,
});
return currentActionId;
}

export function logCdpMessage({
method,
params,
sessionId,
}: {
method: string;
params?: object;
sessionId?: string | null;
}): void {
const args = params ? formatArgs(params) : "";
const call = args ? `${method}(${args})` : `${method}()`;
const prefix = buildPrefix({
includeTask: true,
includeStep: true,
includeAction: true,
});
const rawMessage = `${prefix} ${formatCdpTag(sessionId)} ${call}`;
const message =
rawMessage.length > 120 ? `${rawMessage.slice(0, 117)}...` : rawMessage;
v3Logger({
category: "flow",
message,
level: 2,
});
}

export function clearFlowContext(): void {
currentTaskId = null;
currentStepId = null;
currentActionId = null;
currentStepLabel = null;
currentActionLabel = null;
}
7 changes: 7 additions & 0 deletions packages/core/lib/v3/handlers/handlerUtils/actHandlerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Locator } from "../../understudy/locator";
import { resolveLocatorWithHops } from "../../understudy/deepLocator";
import type { Page } from "../../understudy/page";
import { v3Logger } from "../../logger";
import { logActionProgress } from "../../flowLogger";
import { StagehandClickError } from "../../types/public/sdkErrors";

export class UnderstudyCommandException extends Error {
Expand Down Expand Up @@ -73,6 +74,12 @@ export async function performUnderstudyMethod(
domSettleTimeoutMs,
};

logActionProgress({
actionType: method,
target: selectorRaw,
args: Array.from(args),
});

try {
const handler = METHOD_HANDLER_MAP[method] ?? null;

Expand Down
16 changes: 16 additions & 0 deletions packages/core/lib/v3/handlers/v3CuaAgentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "../types/public/agent";
import { LogLine } from "../types/public/logs";
import { type Action, V3FunctionName } from "../types/public/methods";
import { logActionProgress } from "../flowLogger";

export class V3CuaAgentHandler {
private v3: V3;
Expand Down Expand Up @@ -160,6 +161,21 @@ export class V3CuaAgentHandler {
): Promise<ActionExecutionResult> {
const page = await this.v3.context.awaitActivePage();
const recording = this.v3.isAgentReplayActive();
const pointerTarget =
typeof action.x === "number" && typeof action.y === "number"
? `(${action.x}, ${action.y})`
: typeof action.selector === "string"
? action.selector
: typeof action.input === "string"
? action.input
: typeof action.description === "string"
? action.description
: undefined;
logActionProgress({
actionType: action.type,
target: pointerTarget,
args: [action],
});
switch (action.type) {
case "click": {
const { x, y, button = "left", clickCount } = action;
Expand Down
Loading
Loading