Docs/SDK/Tracing & Debugging

Agent Tracing & Debugging

v0.99.7 - Auto-emit for complete trace structure (run_start, step_start, snapshot) for seamless Studio visualization.

What is a Trace?

“A trace is to a web agent what a commit history is to a Git repo.”

Why Use Traces?

Basic Usage

from predicate import PredicateBrowser, PredicateAgent
from predicate.llm_provider import OpenAIProvider
from predicate.tracing import Tracer, JsonlTraceSink
from predicate.agent_config import AgentConfig

# 1. Create a tracer with JSONL file sink
tracer = Tracer(
    run_id

Trace Events

Each action generates multiple trace events saved to the JSONL file:

Event Types:

Event TypeDescription
run_startAgent run begins (includes agent type, LLM model, config)
step_startAgent begins executing a goal (step_id, goal, attempt)
snapshot / snapshot_takenPage state captured with elements, digests, and diagnostics
llm_called / llm_responseLLM decision made (includes prompt/response hashes, token usage)
action / action_executedAction executed (click, type, press, finish, navigate)
verificationAssertion or verification result (assert, task_done, captcha, scroll)
recoveryRecovery strategy attempted after failure
step_endStep completed (full StepResult with pre/llm/exec/post/verify)
run_endAgent run completed (status: success/failure/partial/unknown)
errorError occurred during step execution

Example trace.jsonl:

{"v":1,"type":"run_start","ts":"2025-12-26T10:00:00.000Z","run_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","seq":1,"data":{"agent":"PredicateAgent","llm_model":"gpt-4o","config":{}}}
{"v":1,"type":"step_start","ts":"2025-12-26T10:00:00.100Z","run_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","seq":2,"step_id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","data":{"step_id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","step_index":1,"goal":"Click the search box","attempt":0,"pre_url":"https://amazon.com"}}
{"v":1,"type":"snapshot_taken","ts":"2025-12-26T10:00:01.000Z","run_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","seq":3,"step_id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","data":{"snapshot_digest":"sha256:abc123...","snapshot_digest_loose":"sha256:def456...","url":"https://amazon.com","element_count":127}}
{"v":1,"type":"llm_called","ts":"2025-12-26T10:00:02.000Z","run_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","seq":4,"step_id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","data":{"step_id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","model":"gpt-4o","response_text":"CLICK(42)","response_hash":"sha256:...","usage":{"prompt_tokens":1523,"completion_tokens":12,"total_tokens":1535}}}
{"v":1,"type":"action_executed","ts":"2025-12-26T10:00:03.000Z","run_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","seq":5,"step_id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","data":{"kind":"click","element_id":42,"success":true,"outcome":"dom_updated","duration_ms":234}}
{"v":1,"type":"step_end","ts":"2025-12-26T10:00:03.500Z","run_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","seq":6,"step_id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","data":{"step_id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","step_index":1,"goal":"Click the search box","attempt":0,"pre":{"url":"https://amazon.com","snapshot_digest":"sha256:abc123..."},"llm":{"response_text":"CLICK(42)","response_hash":"sha256:...","usage":{"prompt_tokens":1523,"completion_tokens":12}},"exec":{"success":true,"outcome":"dom_updated","duration_ms":234},"post":{"url":"https://amazon.com","snapshot_digest":"sha256:xyz789..."},"verify":{"passed":true,"policy":"default"}}}

Snapshot Events for Studio Screenshots

NEW in v0.99.7 - Screenshots are automatically emitted to traces for visualization in Predicate Studio.

Auto-Emit (Default Behavior)

When you call runtime.snapshot(), a snapshot trace event with screenshot_base64 is automatically emitted. This ensures screenshots appear in the Predicate Studio timeline without any extra code.

from predicate.agent_runtime import AgentRuntime

# Default: auto-emit is enabled (emit_trace=True)
snapshot = await runtime.snapshot()  # Automatically emits snapshot trace event

# The trace now includes screenshot_base64 for Studio visualization
# No additional code needed!

Disabling Auto-Emit

If you need manual control over when snapshot events are emitted, disable auto-emit:

# Disable auto-emit for manual control
snapshot = await runtime.snapshot(emit_trace=False)

# Later, manually emit the snapshot when ready
tracer.emit_snapshot(
    snapshot=snapshot,
    step_id=runtime.step_id,
    step_index=

Using emit_snapshot() / emitSnapshot() Directly

The tracer provides a dedicated helper method for emitting snapshot events:

from predicate.tracing import Tracer, JsonlTraceSink

tracer = Tracer(run_id="my-run", sink=JsonlTraceSink("trace.jsonl"))

# Emit snapshot event with screenshot for Studio visualization
tracer.emit_snapshot(
    snapshot=my_snapshot,

Snapshot Event Format

The snapshot event includes these key fields for Studio visualization:

{
  "v": 1,
  "type": "snapshot",
  "ts": "2025-12-26T10:00:01.000Z",
  "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "seq": 3,
  "step_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "data": {
    "url": "https://example.com",
    "element_count": 127,
    "step_index": 1,
    "elements": [...],
    "screenshot_base64": "iVBORw0KGgo...",
    "screenshot_format": "jpeg"
  }
}

Key fields:

When to Use Manual Emit

Manual emission via emit_trace=False is useful when:

  1. Batching snapshots - Taking multiple snapshots but only emitting the final one
  2. Custom processing - Applying PII redaction or other transforms before emission
  3. Conditional emission - Only emitting snapshots that meet certain criteria
  4. Performance optimization - Reducing trace size by selectively emitting snapshots

Screenshot Processing

Apply custom processing (e.g., PII redaction) before screenshots are emitted. The screenshot_processor callback receives a base64-encoded image string and must return a processed base64 string.

Optional Dependencies for Image Processing

For custom screenshot processing (PII redaction, blurring, masking), you may need image processing libraries:

# Python: Pillow is optional but recommended for image manipulation
pip install Pillow

# Or install with the vision-local extras
pip install sentienceapi[vision-local]

Note: The SDK automatically converts non-JPEG screenshots to JPEG format during cloud upload. This conversion requires Pillow in Python. If Pillow is not installed, non-JPEG screenshots will be uploaded in their original format with a warning.

Example: PII Redaction with Image Processing

from predicate.tracer_factory import create_tracer
import base64
from io import BytesIO

def redact_pii(screenshot_base64: str) -> str:
    """Redact PII by blurring sensitive regions."""
    try:

Simple Example (No Dependencies)

If you don't need image manipulation, you can still use the processor for logging or validation:

from predicate.tracer_factory import create_tracer

def log_screenshot(screenshot_base64: str) -> str:
    """Log screenshot size without modifying it."""
    size_kb = len(screenshot_base64) * 3

Complete Trace Structure (Auto-Emit)

NEW in v0.99.7 - The SDK now automatically emits a complete trace structure for proper visualization in Predicate Studio. This includes run_start, step_start, snapshots, and run_end events.

Tracer Factory Auto-Emits run_start

When you create a tracer using create_tracer() / createTracer(), the run_start event is automatically emitted with the metadata you provide. This ensures every trace has a proper starting point for Studio visualization.

from predicate.tracer_factory import create_tracer

# run_start is automatically emitted with metadata
tracer = create_tracer(
    api_key="sk_pro_xxxxx",
    run_id="my-run-123",
    upload_trace=True,
    goal="Add headphones to cart",        # Displayed as trace name in Studio

AgentRuntime Auto-Emits step_start

When you call runtime.begin_step() / runtime.beginStep(), the step_start event is automatically emitted. This ensures each step has a proper starting point in the trace timeline.

from predicate.agent_runtime import AgentRuntime

# step_start is automatically emitted when you begin a step
step_id = runtime.begin_step(
    goal="Click the search button",
    step_index=1,
    pre_url="https://example.com",  # Optional: URL before step
)

Emitting run_end

The run_end event must be emitted manually before closing the tracer to signal the run's completion status:

# After all steps are complete, emit run_end
tracer.emit_run_end(
    steps=total_steps,           # Total number of steps executed
    status="success"             # "success", "failure", "partial", or "unknown"
)

# Then close the tracer to upload
tracer.close()

Complete Trace Lifecycle Example

Here's a complete example showing all auto-emit events in action:

from predicate.tracer_factory import create_tracer
from predicate.agent_runtime import AgentRuntime

# 1. Create tracer - run_start is auto-emitted
tracer = create_tracer(
    api_key="sk_pro_xxxxx",
    run_id="complete-example",
    upload_trace=True,
    goal

Summary of Auto-Emit Behavior

EventAuto-Emit MethodDefaultOpt-Out Parameter
run_startcreate_tracer() / createTracer()Enabledauto_emit_run_start=False / autoEmitRunStart: false
step_startruntime.begin_step() / runtime.beginStep()Enabledemit_trace=False / emitTrace: false
snapshotruntime.snapshot()Enabledemit_trace=False / emitTrace: false
run_endManualN/AN/A (always manual)

AgentConfig Options

Configure agent behavior with AgentConfig:

from predicate.agent_config import AgentConfig

config = AgentConfig(
    # Snapshot settings
    snapshot_limit=50,              # Max elements to include (default: 50)

    # LLM settings
    temperature=0.0,                # LLM temperature 0.0-1.0 (default: 0.0)
    max_retries=1,

Snapshot Utilities

Snapshot Digests (Loop Detection)

Compute fingerprints to detect when page state hasn't changed:

from predicate import snapshot
from predicate.utils import compute_snapshot_digests

snap = snapshot(browser)

# Compute both strict and loose digests
digests = compute_snapshot_digests(snap.elements)

print(digests["strict"]

Digest Types:

LLM Prompt Formatting

Format snapshots for LLM consumption:

from predicate.formatting import format_snapshot_for_llm

snap = snapshot(browser)

# Format top 50 elements for LLM context
llm_context = format_snapshot_for_llm(snap, limit=50)

print(llm_context)
# Output:
# [1] <button> "Sign In" {PRIMARY,CLICKABLE} @ (100,50) (Imp:10)

Format Explanation:

Cloud Tracing & Screenshots

NEW in v0.12.0+ - Upload traces and screenshots to cloud storage for remote viewing, analysis, and collaboration.

Function Signature

The create_tracer() function signature:

def create_tracer(
    api_key: str | None = None,
    run_id: str | None = None,
    api_url: str | None = None,
    logger: PredicateLogger | None = None,
    upload_trace: bool = False,
    goal: str | None = None,
    agent_type: str | None = None,
    llm_model: str | None = None,
    start_url: str | None = None,
    screenshot_processor: Callable[[str], str] | None = None,
)

Overview

Cloud tracing enables Pro, Builder, Teams, and Enterprise tier users to:

Quick Start

from predicate import PredicateBrowser, PredicateAgent
from predicate.llm_provider import OpenAIProvider
from predicate.tracer_factory import create_tracer

# 1. Create tracer with automatic tier detection

tracer = create_tracer(
    api_key="sk_pro_xxxxx",  # Pro/Builder/Teams/Enterprise tier key
    run_id

Automatic Tier Detection

The create_tracer() function automatically detects your tier and configures the appropriate sink:

Pro/Builder/Teams/Enterprise Tier (with API key and upload enabled):

Local-only tracing (opt-out of cloud upload):

Free Tier (no API key):

Graceful Fallback:

Viewing Traces in Cloud

After uploading, access your traces via:

Best Practices

  1. Always close the tracer:

    try:
        # Your agent code
        pass
    finally:
        tracer.close()  # Ensures upload even on errors
  2. Use non-blocking uploads for long-running agents:

    tracer.close(blocking=False)  # Don't wait for upload
  3. Set meaningful run IDs and metadata:

    tracer = create_tracer(
        api_key="sk_pro_xxxxx",
        run_id=f"amazon-shopping-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
        upload_trace=True,
        goal="Buy a laptop from Amazon",
        agent_type="Amazon Shopping Agent",
        llm_model="gpt-4o",
        start_url="https://www.amazon.com",
        screenshot_processor=None, # function for PII redaction
    )
  4. Enable screenshots for debugging:

    config = AgentConfig(capture_screenshots=True)
    agent = PredicateAgent(browser, llm, tracer=tracer, config=config)

Custom Trace Sinks

Implement custom trace storage by extending the TraceSink interface. This allows you to store traces in databases, cloud storage, or any custom backend.

TraceSink Interface

from predicate.tracing import TraceSink

class CustomTraceSink(TraceSink):
    """Base interface for trace storage"""

    def emit(self, event_dict: dict) -> None:

Example: Database Trace Sink

Store traces directly in a database:

from predicate.tracing import TraceSink, Tracer
import psycopg2
import json

class DatabaseTraceSink(TraceSink):
    """Store traces in PostgreSQL database"""

    def __init__(self, connection_string: str

Example: Cloud Storage Sink

Upload traces to S3, Google Cloud Storage, or other cloud providers:

from predicate.tracing import TraceSink
import boto3
import json
from typing import List

class S3TraceSink(TraceSink):
    """Store traces in AWS S3"""

    def __init__(self, bucket:

Example: Multi-Sink (Write to Multiple Destinations)

Write traces to multiple sinks simultaneously:

from predicate.tracing import TraceSink, JsonlTraceSink
from typing import List

class MultiSink(TraceSink):
    """Write to multiple trace sinks simultaneously"""

    def __init__(self, sinks: List[TraceSink

PredicateLogger Interface

Integrate Predicate tracing with your existing logging infrastructure using the PredicateLogger interface.

Logger Interface

from typing import Protocol

class PredicateLogger(Protocol):
    """Protocol for optional logger interface."""

    def info(self, message: str) -> None:
        """Log info message."""

Using Python's Built-in Logger

import logging
from predicate import create_tracer

# Use Python's built-in logging module
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# Add handler to output to console
handler = logging.StreamHandler()

Custom Logger Implementation

from predicate import create_tracer

class CustomLogger:
    """Custom logger implementation"""

    def info(self, message: str) -> None:
        print(f"[INFO] {

What Gets Logged

The logger receives the following types of messages:

Benefits:

Crash Recovery

Traces automatically survive process crashes and are recovered on next SDK initialization.

How It Works

  1. Persistent Cache - Traces are stored in ~/.sentience/traces/pending/ during execution
  2. Process Crash - If your script crashes, traces remain on disk
  3. Automatic Recovery - Next time you create a tracer, orphaned traces are automatically detected
  4. Auto-Upload - Recovered traces are uploaded to cloud (if using Pro/Builder/Teams/Enterprise tier)

Example: Crash and Recovery

from predicate import create_tracer, PredicateBrowser, PredicateAgent
from predicate.llm import OpenAIProvider

# Run 1: Agent crashes mid-execution
tracer = create_tracer(
    api_key="sk_pro_xxxxx",
    run_id="run-1",
    upload_trace=True,

Recovery Details

Best Practices for Crash Recovery

  1. Always use meaningful run IDs:

    run_id = f"shopping-{datetime.now().isoformat()}"
    tracer = create_tracer(
        api_key="sk_pro_xxxxx",
        run_id=run_id,
        upload_trace=True,
        goal="Buy a laptop from Amazon",
        agent_type="Amazon Shopping Agent",
        llm_model="gpt-4o",
        start_url="https://www.amazon.com",
        screenshot_processor=None, # function for PII redaction
    )
  2. Monitor cache directory:

    ls -lh ~/.sentience/traces/pending/
  3. Use try/finally for cleanup:

    tracer = create_tracer(
        api_key="sk_pro_xxxxx",
        run_id="run-123",
        upload_trace=True,
        goal="Complete task",
        agent_type="My Agent",
        llm_model="gpt-4o",
        start_url="https://example.com",
        screenshot_processor=None, # function for PII redaction
    )
    try:
        # Agent code
        agent.act("Do something")
    finally:
        tracer.close()  # Ensures upload even on errors
  4. Check logs for recovery messages:

    • Look for "Found N un-uploaded trace(s)" messages
    • Verify "Uploaded orphaned trace: [run-id]" confirmations

Non-Blocking Trace Uploads

Upload traces in the background to avoid blocking your script execution.

Blocking vs Non-Blocking

from predicate import create_tracer

tracer = create_tracer(
    api_key="sk_pro_xxxxx",
    run_id="run-123",
    upload_trace=True,
    goal="Example task",
    agent_type="Example Agent",
    llm_model="gpt-4o"

Progress Callbacks

Monitor upload progress with callbacks:

def progress_callback(uploaded_bytes: int, total_bytes: int):
    percent = (uploaded_bytes / total_bytes) * 100
    print(f"Upload progress: {percent:

Use Cases

Use non-blocking uploads when:

Use blocking uploads when:

Troubleshooting

"Cloud tracing requires Pro tier"

Cause: Your API key is valid but account is on Free tier

Solution:

"Cloud init timeout"

Cause: Network connectivity issue or API service temporarily unavailable

Solution:

"Upload failed: HTTP 500"

Cause: Server error (temporary)

Solution:

"Local trace preserved"

Cause: Upload failed but trace is safely stored on disk

Solution:

Trace Schema Reference

All trace events follow this structure:

{
  "v": 1,                                              // Schema version (always 1)
  "type": "event_type",                                // Event type (see table below)
  "ts": "2025-12-26T10:00:00.000Z",                    // ISO 8601 timestamp
  "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",   // UUID for the agent run
  "seq": 1,                                            // Monotonically increasing sequence number
  "step_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",  // UUID for the step (optional)
  "data": {...},                                       // Event-specific payload
  "ts_ms": 1735210800000                               // Unix timestamp in milliseconds (optional)
}

⚠️ Warning: Trace formats and internal fields may evolve and are not guaranteed to be stable across versions.

Event-Specific Data Schemas:

run_start - Agent run begins
{
  "agent": "PredicateAgent",
  "llm_model": "gpt-4o",
  "config": {}
}
step_start - Step execution begins
{
  "step_id": "uuid",
  "step_index": 1,
  "goal": "Click the search box",
  "attempt": 0,
  "pre_url": "https://example.com"
}
snapshot / snapshot_taken - Page state captured
{
  "step_id": "uuid",
  "snapshot_id": "uuid",
  "snapshot_digest": "sha256:abc123...",
  "snapshot_digest_loose": "sha256:def456...",
  "url": "https://example.com",
  "element_count": 127,
  "timestamp": "2025-12-26T10:00:01.000Z",
  "diagnostics": {
    "confidence": 0.95,
    "reasons": ["dom_stable", "no_pending_requests"],
    "metrics": {
      "ready_state": "complete",
      "quiet_ms": 500,
      "node_count": 1234,
      "interactive_count": 45
    },
    "captcha": {
      "detected": false,
      "provider_hint": null,
      "confidence": 0.0,
      "evidence": {}
    }
  },
  "elements": [
    {
      "id": 42,
      "role": "button",
      "text": "Search",
      "importance": 950,
      "bbox": {"x": 100, "y": 50, "width": 80, "height": 32},
      "visual_cues": {"is_primary": true, "is_clickable": true},
      "in_viewport": true,
      "is_occluded": false,
      "z_index": 10
    }
  ],
  "screenshot_base64": "iVBORw0KGgo...",
  "screenshot_format": "png"
}
llm_called - LLM query made
{
  "step_id": "uuid",
  "model": "gpt-4o",
  "temperature": 0.0,
  "system_prompt_hash": "sha256:...",
  "user_prompt_hash": "sha256:...",
  "response_text": "CLICK(42)",
  "response_hash": "sha256:...",
  "usage": {
    "prompt_tokens": 1523,
    "completion_tokens": 12,
    "total_tokens": 1535
  }
}
action / action_executed - Action executed
{
  "kind": "click",           // click, type, press, finish, navigate
  "element_id": 42,
  "text": "search query",    // for type action
  "key": "Enter",            // for press action
  "url": "https://...",      // for navigate action
  "raw": "CLICK(42)"
}
verification - Assertion/verification result
{
  "step_id": "uuid",
  "passed": true,
  "kind": "assert",          // assert, task_done, captcha
  "label": "Search box is visible",
  "required": true,
  "reason": "Element found with matching text",
  "details": {},
  "signals": {}
}
recovery - Recovery strategy attempted
{
  "step_id": "uuid",
  "strategy": "retry",
  "attempt": 1
}
step_end - Step completed (full StepResult)
{
  "v": 1,
  "step_id": "uuid",
  "step_index": 1,
  "goal": "Click the search box",
  "attempt": 0,
  "pre": {
    "url": "https://example.com",
    "snapshot_digest": "sha256:abc123...",
    "snapshot_digest_loose": "sha256:def456..."
  },
  "llm": {
    "response_text": "CLICK(42)",
    "response_hash": "sha256:...",
    "usage": {"prompt_tokens": 1523, "completion_tokens": 12, "total_tokens": 1535}
  },
  "action": {
    "kind": "click",
    "element_id": 42,
    "raw": "CLICK(42)"
  },
  "exec": {
    "success": true,
    "outcome": "dom_updated",
    "action": "click",
    "element_id": 42,
    "duration_ms": 234,
    "url_changed": false,
    "bounding_box": {"x": 100, "y": 50, "width": 80, "height": 32}
  },
  "post": {
    "url": "https://example.com",
    "snapshot_digest": "sha256:xyz789...",
    "snapshot_digest_loose": "sha256:uvw123..."
  },
  "verify": {
    "passed": true,
    "policy": "default",
    "signals": {
      "url_changed": false,
      "assertions": [
        {"label": "Element clicked", "passed": true, "required": true}
      ],
      "task_done": false
    }
  },
  "recovery": null
}
run_end - Agent run completed
{
  "steps": 5,
  "status": "success"        // success, failure, partial, unknown
}
error - Error occurred
{
  "step_id": "uuid",
  "attempt": 0,
  "error": "Element not found: id=42"
}

Next Steps