Skip to content

Commit 0af2070

Browse files
authored
Merge pull request #173 from SentienceAPI/improve_tracing
improve tracing with automated upload if opt-in; auto step_start
2 parents 8f017a0 + 21a9708 commit 0af2070

5 files changed

Lines changed: 370 additions & 4 deletions

File tree

src/agent-runtime.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,12 +626,25 @@ export class AgentRuntime {
626626
* Take a snapshot of the current page state.
627627
*
628628
* This updates lastSnapshot which is used as context for assertions.
629+
* When emitTrace=true (default), automatically emits a 'snapshot' trace event
630+
* with screenshot_base64 for Sentience Studio visualization.
629631
*
630632
* @param options - Options passed through to browser.snapshot()
633+
* @param options.emitTrace - If true (default), emit a 'snapshot' trace event with screenshot.
634+
* Set to false to disable automatic trace emission.
631635
* @returns Snapshot of current page state
636+
*
637+
* @example
638+
* // Default: snapshot with auto-emit trace event
639+
* const snapshot = await runtime.snapshot();
640+
*
641+
* // Disable auto-emit for manual control
642+
* const snapshot = await runtime.snapshot({ emitTrace: false });
643+
* // Later, manually emit if needed:
644+
* tracer.emitSnapshot(snapshot, runtime.getStepId());
632645
*/
633646
async snapshot(options?: Record<string, any>): Promise<Snapshot> {
634-
const { _skipCaptchaHandling, ...snapshotOptions } = options || {};
647+
const { _skipCaptchaHandling, emitTrace = true, ...snapshotOptions } = options || {};
635648
this.lastSnapshot = await this.browser.snapshot(this.page, snapshotOptions);
636649
if (this.lastSnapshot && !this.stepPreSnapshot) {
637650
this.stepPreSnapshot = this.lastSnapshot;
@@ -640,9 +653,32 @@ export class AgentRuntime {
640653
if (!_skipCaptchaHandling) {
641654
await this.handleCaptchaIfNeeded(this.lastSnapshot, 'gateway');
642655
}
656+
657+
// Auto-emit snapshot trace event for Studio visualization
658+
if (emitTrace && this.lastSnapshot && this.tracer) {
659+
this.emitSnapshotTrace(this.lastSnapshot);
660+
}
661+
643662
return this.lastSnapshot;
644663
}
645664

665+
/**
666+
* Emit a snapshot trace event with screenshot for Studio visualization.
667+
*
668+
* This is called automatically by snapshot() when emitTrace=true.
669+
*/
670+
private emitSnapshotTrace(snapshot: Snapshot): void {
671+
if (!this.tracer) {
672+
return;
673+
}
674+
675+
try {
676+
this.tracer.emitSnapshot(snapshot, this.stepId ?? undefined, this.stepIndex, 'jpeg');
677+
} catch {
678+
// Best-effort: don't let trace emission errors break snapshot
679+
}
680+
}
681+
646682
/**
647683
* Evaluate JavaScript in the page context.
648684
*/
@@ -1167,12 +1203,20 @@ export class AgentRuntime {
11671203
* - Generates a new stepId
11681204
* - Clears assertions from previous step
11691205
* - Increments stepIndex (or uses provided value)
1206+
* - Emits step_start trace event (optional)
11701207
*
11711208
* @param goal - Description of what this step aims to achieve
11721209
* @param stepIndex - Optional explicit step index (otherwise auto-increments)
1210+
* @param options - Optional settings: emitTrace (default true), preUrl
11731211
* @returns Generated stepId in format 'step-N' where N is the step index
11741212
*/
1175-
beginStep(goal: string, stepIndex?: number): string {
1213+
beginStep(
1214+
goal: string,
1215+
stepIndex?: number,
1216+
options?: { emitTrace?: boolean; preUrl?: string }
1217+
): string {
1218+
const { emitTrace = true, preUrl } = options || {};
1219+
11761220
// Clear previous step state
11771221
this.assertionsThisStep = [];
11781222
this.stepPreSnapshot = null;
@@ -1190,6 +1234,16 @@ export class AgentRuntime {
11901234
// Generate stepId in 'step-N' format for Studio compatibility
11911235
this.stepId = `step-${this.stepIndex}`;
11921236

1237+
// Emit step_start trace event for Studio timeline display
1238+
if (emitTrace && this.tracer) {
1239+
try {
1240+
const url = preUrl || this.lastSnapshot?.url || this.page?.url?.() || '';
1241+
this.tracer.emitStepStart(this.stepId, this.stepIndex, goal, 0, url);
1242+
} catch {
1243+
// Tracing must be non-fatal
1244+
}
1245+
}
1246+
11931247
return this.stepId;
11941248
}
11951249

src/tracing/tracer-factory.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,35 @@ import { Tracer } from './tracer';
1919
import { CloudTraceSink, SentienceLogger } from './cloud-sink';
2020
import { JsonlTraceSink } from './jsonl-sink';
2121

22+
/**
23+
* Helper to emit run_start event with available metadata
24+
*/
25+
function emitRunStart(
26+
tracer: Tracer,
27+
agentType?: string,
28+
llmModel?: string,
29+
goal?: string,
30+
startUrl?: string
31+
): void {
32+
try {
33+
const config: Record<string, string> = {};
34+
if (goal) {
35+
config.goal = goal;
36+
}
37+
if (startUrl) {
38+
config.start_url = startUrl;
39+
}
40+
41+
tracer.emitRunStart(
42+
agentType || 'SentienceAgent',
43+
llmModel,
44+
Object.keys(config).length > 0 ? config : undefined
45+
);
46+
} catch {
47+
// Tracing must be non-fatal
48+
}
49+
}
50+
2251
/**
2352
* Sentience API base URL (constant)
2453
*/
@@ -198,6 +227,7 @@ function httpPost(
198227
* @param options.llmModel - LLM model used (e.g., "gpt-4-turbo", "claude-3-5-sonnet")
199228
* @param options.startUrl - Starting URL of the agent run (e.g., "https://amazon.com")
200229
* @param options.screenshotProcessor - Optional function to process screenshots before upload. Takes base64 string, returns processed base64 string. Useful for PII redaction or custom image processing.
230+
* @param options.autoEmitRunStart - If true (default), automatically emit run_start event with provided metadata. This ensures traces have complete structure for Studio visualization.
201231
* @returns Tracer configured with appropriate sink
202232
*
203233
* @example
@@ -213,6 +243,7 @@ function httpPost(
213243
* uploadTrace: true
214244
* });
215245
* // Returns: Tracer with CloudTraceSink
246+
* // run_start event is automatically emitted
216247
*
217248
* // With screenshot processor for PII redaction
218249
* const redactPII = (screenshot: string): string => {
@@ -229,6 +260,10 @@ function httpPost(
229260
* const tracer = await createTracer({ apiKey: "sk_pro_xyz", runId: "demo", uploadTrace: false });
230261
* // Returns: Tracer with JsonlTraceSink (local-only)
231262
*
263+
* // Disable auto-emit for manual control
264+
* const tracer = await createTracer({ runId: "demo", autoEmitRunStart: false });
265+
* tracer.emitRunStart("MyAgent", "gpt-4o"); // Manual emit
266+
*
232267
* // Free tier user
233268
* const tracer = await createTracer({ runId: "demo" });
234269
* // Returns: Tracer with JsonlTraceSink (local-only)
@@ -250,6 +285,7 @@ export async function createTracer(options: {
250285
llmModel?: string;
251286
startUrl?: string;
252287
screenshotProcessor?: (screenshot: string) => string;
288+
autoEmitRunStart?: boolean;
253289
}): Promise<Tracer> {
254290
const runId = options.runId || randomUUID();
255291
const apiUrl = options.apiUrl || SENTIENCE_API_URL;
@@ -303,11 +339,18 @@ export async function createTracer(options: {
303339

304340
console.log('☁️ [Sentience] Cloud tracing enabled (Pro tier)');
305341
// PRODUCTION FIX: Pass runId for persistent cache naming
306-
return new Tracer(
342+
const tracer = new Tracer(
307343
runId,
308344
new CloudTraceSink(uploadUrl, runId, options.apiKey, apiUrl, options.logger),
309345
options.screenshotProcessor
310346
);
347+
348+
// Auto-emit run_start for complete trace structure
349+
if (options.autoEmitRunStart !== false) {
350+
emitRunStart(tracer, options.agentType, options.llmModel, options.goal, options.startUrl);
351+
}
352+
353+
return tracer;
311354
} else if (response.status === 403) {
312355
console.log('⚠️ [Sentience] Cloud tracing requires Pro tier');
313356
console.log(' Falling back to local-only tracing');
@@ -338,7 +381,14 @@ export async function createTracer(options: {
338381
const localPath = path.join(tracesDir, `${runId}.jsonl`);
339382
console.log(`💾 [Sentience] Local tracing: ${localPath}`);
340383

341-
return new Tracer(runId, new JsonlTraceSink(localPath), options.screenshotProcessor);
384+
const tracer = new Tracer(runId, new JsonlTraceSink(localPath), options.screenshotProcessor);
385+
386+
// Auto-emit run_start for complete trace structure
387+
if (options.autoEmitRunStart !== false) {
388+
emitRunStart(tracer, options.agentType, options.llmModel, options.goal, options.startUrl);
389+
}
390+
391+
return tracer;
342392
}
343393

344394
/**

src/tracing/tracer.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,93 @@ export class Tracer {
202202
this.emit('error', { step_id: stepId, error, attempt }, stepId);
203203
}
204204

205+
/**
206+
* Emit snapshot event with screenshot for Studio visualization.
207+
*
208+
* This method builds and emits a 'snapshot' trace event that includes:
209+
* - Page URL and element data
210+
* - Screenshot (if present in snapshot)
211+
* - Step correlation info
212+
*
213+
* Use this when you want screenshots to appear in the Sentience Studio timeline.
214+
*
215+
* @param snapshot - Snapshot object (must have 'screenshot' property for images)
216+
* @param stepId - Step UUID (for correlating snapshot with a step)
217+
* @param stepIndex - Step index (0-based) for Studio timeline ordering
218+
* @param screenshotFormat - Format of screenshot ("jpeg" or "png", default: "jpeg")
219+
*
220+
* @example
221+
* // After taking a snapshot with AgentRuntime
222+
* const snapshot = await runtime.snapshot();
223+
* tracer.emitSnapshot(snapshot, runtime.getStepId(), runtime.getStepIndex());
224+
*
225+
* // Or use auto-emit (default in AgentRuntime.snapshot())
226+
* const snapshot = await runtime.snapshot(); // Auto-emits snapshot event
227+
*/
228+
emitSnapshot(
229+
snapshot: any,
230+
stepId?: string,
231+
stepIndex?: number,
232+
screenshotFormat: string = 'jpeg'
233+
): void {
234+
if (!snapshot) {
235+
return;
236+
}
237+
238+
try {
239+
// Build the snapshot event data
240+
const data: TraceEventData = {
241+
url: snapshot.url,
242+
element_count: snapshot.elements?.length || 0,
243+
timestamp: snapshot.timestamp,
244+
};
245+
246+
// Include step_index if provided (required for UUID step_ids)
247+
if (stepIndex !== undefined) {
248+
data.step_index = stepIndex;
249+
}
250+
251+
// Include elements data (simplified for trace)
252+
if (snapshot.elements && snapshot.elements.length > 0) {
253+
// Normalize importance values to importance_score (0-1 range)
254+
const importanceValues = snapshot.elements.map((el: any) => el.importance || 0);
255+
const minImportance = Math.min(...importanceValues);
256+
const maxImportance = Math.max(...importanceValues);
257+
const importanceRange = maxImportance - minImportance;
258+
259+
data.elements = snapshot.elements.map((el: any) => {
260+
const importanceScore =
261+
importanceRange > 0 ? (el.importance - minImportance) / importanceRange : 0.5;
262+
return {
263+
...el,
264+
importance_score: importanceScore,
265+
};
266+
});
267+
}
268+
269+
// Extract and add screenshot if present
270+
const screenshotRaw = snapshot.screenshot;
271+
if (screenshotRaw) {
272+
// Extract base64 string from data URL if needed
273+
// Format: "data:image/jpeg;base64,{base64_string}"
274+
let screenshotBase64: string;
275+
if (typeof screenshotRaw === 'string' && screenshotRaw.startsWith('data:image')) {
276+
const commaIndex = screenshotRaw.indexOf(',');
277+
screenshotBase64 =
278+
commaIndex !== -1 ? screenshotRaw.slice(commaIndex + 1) : screenshotRaw;
279+
} else {
280+
screenshotBase64 = screenshotRaw;
281+
}
282+
data.screenshot_base64 = screenshotBase64;
283+
data.screenshot_format = screenshotFormat;
284+
}
285+
286+
this.emit('snapshot', data, stepId);
287+
} catch {
288+
// Best-effort: don't let trace emission errors break the caller
289+
}
290+
}
291+
205292
/**
206293
* Automatically infer finalStatus from tracked step outcomes if not explicitly set.
207294
* This is called automatically in close() if finalStatus is still "unknown".

tests/tracing/tracer-factory.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ describe('createTracer', () => {
305305
it('should work with agent workflow (Free tier)', async () => {
306306
const tracer = await createTracer({
307307
runId: 'agent-test',
308+
autoEmitRunStart: false, // Disable auto-emit since we're manually emitting
308309
});
309310

310311
tracer.emitRunStart('SentienceAgent', 'gpt-4');

0 commit comments

Comments
 (0)