Skip to content
Merged
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
121 changes: 120 additions & 1 deletion src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { click, typeText, press } from './actions';
import { Snapshot, Element, ActionResult } from './types';
import { LLMProvider, LLMResponse } from './llm-provider';
import { Tracer } from './tracing/tracer';
import { randomUUID } from 'crypto';
import { randomUUID, createHash } from 'crypto';

/**
* Execution result from agent.act()
Expand Down Expand Up @@ -124,6 +124,28 @@ export class SentienceAgent {

}

/**
* Compute SHA256 hash of text
*/
private computeHash(text: string): string {
return createHash('sha256').update(text, 'utf8').digest('hex');
}

/**
* Get bounding box for an element from snapshot
*/
private getElementBbox(elementId: number | undefined, snap: Snapshot): { x: number; y: number; width: number; height: number } | undefined {
if (elementId === undefined) return undefined;
const el = snap.elements.find(e => e.id === elementId);
if (!el) return undefined;
return {
x: el.bbox.x,
y: el.bbox.y,
width: el.bbox.width,
height: el.bbox.height,
};
}

/**
* Execute a high-level goal using observe → think → act loop
* @param goal - Natural language instruction (e.g., "Click the Sign In button")
Expand Down Expand Up @@ -288,6 +310,103 @@ export class SentienceAgent {
console.log(`${status} Completed in ${durationMs}ms`);
}

// Emit step_end event if tracer is enabled
if (this.tracer) {
const preUrl = snap.url;
const postUrl = this.browser.getPage()?.url() || null;

// Compute snapshot digest (simplified - use URL + timestamp)
const snapshotDigest = `sha256:${this.computeHash(`${preUrl}${snap.timestamp}`)}`;

// Build LLM data
const llmResponseText = llmResponse.content;
const llmResponseHash = `sha256:${this.computeHash(llmResponseText)}`;
const llmData = {
response_text: llmResponseText,
response_hash: llmResponseHash,
usage: {
prompt_tokens: llmResponse.promptTokens || 0,
completion_tokens: llmResponse.completionTokens || 0,
total_tokens: llmResponse.totalTokens || 0,
},
};

// Build exec data
const execData: any = {
success: result.success,
action: result.action || 'unknown',
outcome: result.outcome || (result.success ? `Action ${result.action || 'unknown'} executed successfully` : `Action ${result.action || 'unknown'} failed`),
duration_ms: durationMs,
};

// Add optional exec fields
if (result.elementId !== undefined) {
execData.element_id = result.elementId;
// Add bounding box if element found
const bbox = this.getElementBbox(result.elementId, snap);
if (bbox) {
execData.bounding_box = bbox;
}
}
if (result.text !== undefined) {
execData.text = result.text;
}
if (result.key !== undefined) {
execData.key = result.key;
}
if (result.error !== undefined) {
execData.error = result.error;
}

// Build verify data (simplified - based on success and url_changed)
const verifyPassed = result.success && (result.urlChanged || result.action !== 'click');
const verifySignals: any = {
url_changed: result.urlChanged || false,
};
if (result.error) {
verifySignals.error = result.error;
}

// Add elements_found array if element was targeted
if (result.elementId !== undefined) {
const bbox = this.getElementBbox(result.elementId, snap);
if (bbox) {
verifySignals.elements_found = [
{
label: `Element ${result.elementId}`,
bounding_box: bbox,
},
];
}
}

const verifyData = {
passed: verifyPassed,
signals: verifySignals,
};

// Build complete step_end event
const stepEndData = {
v: 1,
step_id: stepId,
step_index: this.stepCount,
goal: goal,
attempt: attempt,
pre: {
url: preUrl,
snapshot_digest: snapshotDigest,
},
llm: llmData,
exec: execData,
post: {
url: postUrl,
},
verify: verifyData,
};

this.tracer.emit('step_end', stepEndData, stepId);
}

return result;

} catch (error: any) {
Expand Down
87 changes: 84 additions & 3 deletions src/tracing/index-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ export class TraceFileInfo {
constructor(
public path: string,
public size_bytes: number,
public sha256: string
public sha256: string,
public line_count: number | null = null // Number of lines in the trace file
) {}

toJSON() {
return {
path: this.path,
size_bytes: this.size_bytes,
sha256: this.sha256,
line_count: this.line_count,
};
}
}
Expand All @@ -25,7 +27,11 @@ export class TraceSummary {
public event_count: number,
public step_count: number,
public error_count: number,
public final_url: string | null
public final_url: string | null,
public status: 'success' | 'failure' | 'partial' | 'unknown' | null = null,
public agent_name: string | null = null, // Agent name from run_start event
public duration_ms: number | null = null, // Calculated duration in milliseconds
public counters: { snapshot_count: number; action_count: number; error_count: number } | null = null // Aggregated counters
) {}

toJSON() {
Expand All @@ -36,6 +42,10 @@ export class TraceSummary {
step_count: this.step_count,
error_count: this.error_count,
final_url: this.final_url,
status: this.status,
agent_name: this.agent_name,
duration_ms: this.duration_ms,
counters: this.counters,
};
}
}
Expand Down Expand Up @@ -92,7 +102,7 @@ export class StepCounters {
}
}

export type StepStatus = 'ok' | 'error' | 'partial';
export type StepStatus = 'success' | 'failure' | 'partial' | 'unknown';

export class StepIndex {
constructor(
Expand All @@ -104,6 +114,7 @@ export class StepIndex {
public ts_end: string,
public offset_start: number,
public offset_end: number,
public line_number: number | null = null, // Line number for byte-range fetching
public url_before: string | null,
public url_after: string | null,
public snapshot_before: SnapshotInfo,
Expand All @@ -122,6 +133,7 @@ export class StepIndex {
ts_end: this.ts_end,
offset_start: this.offset_start,
offset_end: this.offset_end,
line_number: this.line_number,
url_before: this.url_before,
url_after: this.url_after,
snapshot_before: this.snapshot_before.toJSON(),
Expand Down Expand Up @@ -152,4 +164,73 @@ export class TraceIndex {
steps: this.steps.map((s) => s.toJSON()),
};
}

/**
* Convert to SS format.
*
* Maps SDK field names to frontend expectations:
* - created_at -> generated_at
* - first_ts -> start_time
* - last_ts -> end_time
* - step_index -> step (already 1-based, good!)
* - ts_start -> timestamp
* - Filters out "unknown" status
*/
toSentienceStudioJSON(): any {
// Calculate duration if not already set
let durationMs = this.summary.duration_ms;
if (durationMs === null && this.summary.first_ts && this.summary.last_ts) {
const start = new Date(this.summary.first_ts);
const end = new Date(this.summary.last_ts);
durationMs = end.getTime() - start.getTime();
}

// Aggregate counters if not already set
let counters = this.summary.counters;
if (counters === null) {
const snapshotCount = this.steps.reduce((sum, s) => sum + s.counters.snapshots, 0);
const actionCount = this.steps.reduce((sum, s) => sum + s.counters.actions, 0);
counters = {
snapshot_count: snapshotCount,
action_count: actionCount,
error_count: this.summary.error_count,
};
}

return {
version: this.version,
run_id: this.run_id,
generated_at: this.created_at, // Renamed from created_at
trace_file: {
path: this.trace_file.path,
size_bytes: this.trace_file.size_bytes,
line_count: this.trace_file.line_count, // Added
},
summary: {
agent_name: this.summary.agent_name, // Added
total_steps: this.summary.step_count, // Renamed from step_count
status: this.summary.status !== 'unknown' ? this.summary.status : null, // Filter out unknown
start_time: this.summary.first_ts, // Renamed from first_ts
end_time: this.summary.last_ts, // Renamed from last_ts
duration_ms: durationMs, // Added
counters: counters, // Added
},
steps: this.steps.map((s) => ({
step: s.step_index, // Already 1-based ✅
byte_offset: s.offset_start,
line_number: s.line_number, // Added
timestamp: s.ts_start, // Use start time
action: {
type: s.action.type || '',
goal: s.goal, // Move goal into action
digest: s.action.args_digest,
},
snapshot: s.snapshot_after.url ? {
url: s.snapshot_after.url,
digest: s.snapshot_after.digest,
} : undefined,
status: s.status !== 'unknown' ? s.status : undefined, // Filter out unknown
})),
};
}
}
Loading