Skip to content

Commit b74dd98

Browse files
committed
add new flow logger
1 parent 44bb4f5 commit b74dd98

File tree

6 files changed

+373
-0
lines changed

6 files changed

+373
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Stagehand } from "../lib/v3";
2+
3+
async function run(): Promise<void> {
4+
const apiKey = process.env.OPENAI_API_KEY;
5+
if (!apiKey) {
6+
throw new Error(
7+
"Set OPENAI_API_KEY to a valid OpenAI key before running this demo.",
8+
);
9+
}
10+
11+
const stagehand = new Stagehand({
12+
env: "LOCAL",
13+
verbose: 2,
14+
model: { modelName: "openai/gpt-4.1-mini", apiKey },
15+
localBrowserLaunchOptions: {
16+
headless: true,
17+
args: ["--window-size=1280,720"],
18+
},
19+
disablePino: true,
20+
});
21+
22+
try {
23+
await stagehand.init();
24+
25+
const [page] = stagehand.context.pages();
26+
await page.goto("https://example.com/", { waitUntil: "load" });
27+
28+
const agent = stagehand.agent({
29+
systemPrompt:
30+
"You are a QA assistant. Keep answers short and deterministic. Finish quickly.",
31+
});
32+
const agentResult = await agent.execute(
33+
"Glance at the Example Domain page and confirm that you see the hero text.",
34+
);
35+
console.log("Agent result:", agentResult);
36+
37+
const observations = await stagehand.observe(
38+
"Locate the 'More information...' link on this page.",
39+
);
40+
console.log("Observe result:", observations);
41+
42+
if (observations.length > 0) {
43+
await stagehand.act(observations[0]);
44+
} else {
45+
await stagehand.act("click the link labeled 'More information...'");
46+
}
47+
48+
const extraction = await stagehand.extract(
49+
"Summarize the current page title and URL.",
50+
);
51+
console.log("Extraction result:", extraction);
52+
} finally {
53+
await stagehand.close({ force: true }).catch(() => {});
54+
}
55+
}
56+
57+
run().catch((error) => {
58+
console.error(error);
59+
process.exitCode = 1;
60+
});

packages/core/lib/v3/flowLogger.ts

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import { randomUUID } from "node:crypto";
2+
import { v3Logger } from "./logger";
3+
4+
type FlowPrefixOptions = {
5+
includeAction?: boolean;
6+
includeStep?: boolean;
7+
includeTask?: boolean;
8+
};
9+
10+
const MAX_ARG_LENGTH = 500;
11+
12+
let currentTaskId: string | null = null;
13+
let currentStepId: string | null = null;
14+
let currentActionId: string | null = null;
15+
let currentStepLabel: string | null = null;
16+
let currentActionLabel: string | null = null;
17+
18+
function generateId(label: string): string {
19+
try {
20+
return randomUUID();
21+
} catch {
22+
const fallback =
23+
(globalThis.crypto as Crypto | undefined)?.randomUUID?.() ??
24+
`${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
25+
return fallback;
26+
}
27+
}
28+
29+
function truncate(value: string): string {
30+
if (value.length <= MAX_ARG_LENGTH) {
31+
return value;
32+
}
33+
return `${value.slice(0, MAX_ARG_LENGTH)}…`;
34+
}
35+
36+
function formatValue(value: unknown): string {
37+
if (typeof value === "string") {
38+
return `'${value}'`;
39+
}
40+
if (
41+
typeof value === "number" ||
42+
typeof value === "boolean" ||
43+
value === null
44+
) {
45+
return String(value);
46+
}
47+
if (Array.isArray(value)) {
48+
try {
49+
return truncate(JSON.stringify(value));
50+
} catch {
51+
return "[unserializable array]";
52+
}
53+
}
54+
if (typeof value === "object" && value !== null) {
55+
try {
56+
return truncate(JSON.stringify(value));
57+
} catch {
58+
return "[unserializable object]";
59+
}
60+
}
61+
if (value === undefined) {
62+
return "undefined";
63+
}
64+
return truncate(String(value));
65+
}
66+
67+
function formatArgs(args?: unknown | unknown[]): string {
68+
if (args === undefined) {
69+
return "";
70+
}
71+
const normalized = Array.isArray(args) ? args : [args];
72+
const rendered = normalized
73+
.map((entry) => formatValue(entry))
74+
.filter((entry) => entry.length > 0);
75+
return rendered.join(", ");
76+
}
77+
78+
function formatTag(label: string, id: string | null): string {
79+
return `[${label} #${shortId(id)}]`;
80+
}
81+
82+
function formatCdpTag(sessionId?: string | null): string {
83+
if (!sessionId) return "[CDP]";
84+
return `[CDP #${shortId(sessionId).toUpperCase()}]`;
85+
}
86+
87+
function shortId(id: string | null): string {
88+
if (!id) return "-";
89+
const trimmed = id.slice(-4);
90+
return trimmed;
91+
}
92+
93+
function ensureTaskContext(): void {
94+
if (!currentTaskId) {
95+
currentTaskId = generateId("task");
96+
}
97+
}
98+
99+
function ensureStepContext(defaultLabel?: string): void {
100+
if (defaultLabel) {
101+
currentStepLabel = defaultLabel.toUpperCase();
102+
}
103+
if (!currentStepLabel) {
104+
currentStepLabel = "STEP";
105+
}
106+
if (!currentStepId) {
107+
currentStepId = generateId("step");
108+
}
109+
}
110+
111+
function ensureActionContext(defaultLabel?: string): void {
112+
if (defaultLabel) {
113+
currentActionLabel = defaultLabel.toUpperCase();
114+
}
115+
if (!currentActionLabel) {
116+
currentActionLabel = "ACTION";
117+
}
118+
if (!currentActionId) {
119+
currentActionId = generateId("action");
120+
}
121+
}
122+
123+
function buildPrefix({
124+
includeAction = true,
125+
includeStep = true,
126+
includeTask = true,
127+
}: FlowPrefixOptions = {}): string {
128+
const parts: string[] = [];
129+
if (includeTask) {
130+
ensureTaskContext();
131+
parts.push(formatTag("TASK", currentTaskId));
132+
}
133+
if (includeStep) {
134+
ensureStepContext();
135+
const label = currentStepLabel ?? "STEP";
136+
parts.push(formatTag(label, currentStepId));
137+
}
138+
if (includeAction) {
139+
ensureActionContext();
140+
const actionLabel = currentActionLabel ?? "ACTION";
141+
parts.push(formatTag(actionLabel, currentActionId));
142+
}
143+
return parts.join(" ");
144+
}
145+
146+
export function logTaskProgress({
147+
invocation,
148+
args,
149+
}: {
150+
invocation: string;
151+
args?: unknown | unknown[];
152+
}): string {
153+
currentTaskId = generateId("task");
154+
currentStepId = null;
155+
currentActionId = null;
156+
currentStepLabel = null;
157+
currentActionLabel = null;
158+
159+
const call = `${invocation}(${formatArgs(args)})`;
160+
const message = `${buildPrefix({
161+
includeTask: true,
162+
includeStep: false,
163+
includeAction: false,
164+
})} ${call}`;
165+
v3Logger({
166+
category: "flow",
167+
message,
168+
level: 2,
169+
});
170+
return currentTaskId;
171+
}
172+
173+
export function logStepProgress({
174+
invocation,
175+
args,
176+
label,
177+
}: {
178+
invocation: string;
179+
args?: unknown | unknown[];
180+
label: string;
181+
}): string {
182+
ensureTaskContext();
183+
currentStepId = generateId("step");
184+
currentStepLabel = label.toUpperCase();
185+
currentActionId = null;
186+
currentActionLabel = null;
187+
188+
const call = `${invocation}(${formatArgs(args)})`;
189+
const message = `${buildPrefix({
190+
includeTask: true,
191+
includeStep: true,
192+
includeAction: false,
193+
})} ${call}`;
194+
v3Logger({
195+
category: "flow",
196+
message,
197+
level: 2,
198+
});
199+
return currentStepId;
200+
}
201+
202+
export function logActionProgress({
203+
actionType,
204+
target,
205+
args,
206+
}: {
207+
actionType: string;
208+
target?: string;
209+
args?: unknown | unknown[];
210+
}): string {
211+
ensureTaskContext();
212+
ensureStepContext();
213+
currentActionId = generateId("action");
214+
currentActionLabel = actionType.toUpperCase();
215+
const details: string[] = [`${actionType}`];
216+
if (target) {
217+
details.push(`target=${target}`);
218+
}
219+
const argString = formatArgs(args);
220+
if (argString) {
221+
details.push(`args=[${argString}]`);
222+
}
223+
224+
const message = `${buildPrefix({
225+
includeTask: true,
226+
includeStep: true,
227+
includeAction: true,
228+
})} ${details.join(" ")}`;
229+
v3Logger({
230+
category: "flow",
231+
message,
232+
level: 2,
233+
});
234+
return currentActionId;
235+
}
236+
237+
export function logCdpMessage({
238+
method,
239+
params,
240+
sessionId,
241+
}: {
242+
method: string;
243+
params?: object;
244+
sessionId?: string | null;
245+
}): void {
246+
const args = params ? formatArgs(params) : "";
247+
const call = args ? `${method}(${args})` : `${method}()`;
248+
const prefix = buildPrefix({
249+
includeTask: true,
250+
includeStep: true,
251+
includeAction: true,
252+
});
253+
const rawMessage = `${prefix} ${formatCdpTag(sessionId)} ${call}`;
254+
const message =
255+
rawMessage.length > 120 ? `${rawMessage.slice(0, 117)}...` : rawMessage;
256+
v3Logger({
257+
category: "flow",
258+
message,
259+
level: 2,
260+
});
261+
}
262+
263+
export function clearFlowContext(): void {
264+
currentTaskId = null;
265+
currentStepId = null;
266+
currentActionId = null;
267+
currentStepLabel = null;
268+
currentActionLabel = null;
269+
}

packages/core/lib/v3/handlers/handlerUtils/actHandlerUtils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Locator } from "../../understudy/locator";
55
import { resolveLocatorWithHops } from "../../understudy/deepLocator";
66
import type { Page } from "../../understudy/page";
77
import { v3Logger } from "../../logger";
8+
import { logActionProgress } from "../../flowLogger";
89
import { StagehandClickError } from "../../types/public/sdkErrors";
910

1011
export class UnderstudyCommandException extends Error {
@@ -73,6 +74,12 @@ export async function performUnderstudyMethod(
7374
domSettleTimeoutMs,
7475
};
7576

77+
logActionProgress({
78+
actionType: method,
79+
target: selectorRaw,
80+
args: Array.from(args),
81+
});
82+
7683
try {
7784
const handler = METHOD_HANDLER_MAP[method] ?? null;
7885

packages/core/lib/v3/handlers/v3CuaAgentHandler.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "../types/public/agent";
1414
import { LogLine } from "../types/public/logs";
1515
import { type Action, V3FunctionName } from "../types/public/methods";
16+
import { logActionProgress } from "../flowLogger";
1617

1718
export class V3CuaAgentHandler {
1819
private v3: V3;
@@ -160,6 +161,15 @@ export class V3CuaAgentHandler {
160161
): Promise<ActionExecutionResult> {
161162
const page = await this.v3.context.awaitActivePage();
162163
const recording = this.v3.isAgentReplayActive();
164+
const pointerTarget =
165+
typeof action.x === "number" && typeof action.y === "number"
166+
? `(${action.x}, ${action.y})`
167+
: action.selector || action.input || action.description;
168+
logActionProgress({
169+
actionType: action.type,
170+
target: pointerTarget,
171+
args: [action],
172+
});
163173
switch (action.type) {
164174
case "click": {
165175
const { x, y, button = "left", clickCount } = action;

packages/core/lib/v3/understudy/cdp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// lib/v3/understudy/cdp.ts
22
import WebSocket from "ws";
33
import type { Protocol } from "devtools-protocol";
4+
import { logCdpMessage } from "../flowLogger";
45

56
/**
67
* CDP transport & session multiplexer
@@ -118,6 +119,7 @@ export class CdpConnection implements CDPSessionLike {
118119
ts: Date.now(),
119120
});
120121
});
122+
logCdpMessage({ method, params, sessionId: null });
121123
this.ws.send(JSON.stringify(payload));
122124
return p;
123125
}
@@ -232,6 +234,7 @@ export class CdpConnection implements CDPSessionLike {
232234
ts: Date.now(),
233235
});
234236
});
237+
logCdpMessage({ method, params, sessionId });
235238
this.ws.send(JSON.stringify(payload));
236239
return p;
237240
}

0 commit comments

Comments
 (0)