Skip to content

Events

The Event class is the central object in spanforge. Every interaction between an LLM tool and the outside world — a trace span, a cost record, a prompt save, a guard block — is represented as an Event.

The event envelope

Every event carries a common envelope regardless of the event type:

FieldRequiredDescription
schema_versionAutoDefaults to "2.0" (consumers also accept "1.0" for backward compatibility).
event_idAutoULID (26-character, time-ordered, URL-safe). Auto-generated if omitted.
event_typeYesEvent type string. Use a registered first-party EventType value (RFC Appendix B) or a valid reverse-domain custom type outside llm.* (e.g. x.company.entity.action).
timestampAutoUTC ISO-8601 (YYYY-MM-DDTHH:MM:SS.ffffffZ). Auto-generated.
sourceYesEmitting tool in "name@semver" format, e.g. "llm-trace@1.0.0".
payloadYesTool-specific dict. Must be non-empty. All values must be JSON-serialisable.
trace_idOptional32-char lowercase hex OpenTelemetry trace ID.
span_idOptional16-char lowercase hex OpenTelemetry span ID.
parent_span_idOptional16-char lowercase hex parent span ID.
org_idOptionalOrganisation identifier for multi-tenant deployments.
team_idOptionalTeam identifier.
actor_idOptionalUser or service-account identifier.
session_idOptionalSession identifier grouping related events.
tagsOptionalArbitrary str → str metadata via Tags.
checksumSigningSHA-256 payload checksum. Set by sign.
signatureSigningHMAC-SHA256 chain signature. Set by sign.
prev_idSigningevent_id of the preceding event in the audit chain.

Event is an immutable envelope class with read-only properties after construction.

Event types

All first-party event types are members of the EventType enum:

from spanforge import EventType

# Trace namespace
EventType.TRACE_SPAN_COMPLETED       # "llm.trace.span.completed"
EventType.TRACE_SPAN_FAILED          # "llm.trace.span.failed"

# Cost namespace
EventType.COST_TOKEN_RECORDED        # "llm.cost.token.recorded"
EventType.COST_SESSION_RECORDED      # "llm.cost.session.recorded"

# Guard namespace
EventType.GUARD_INPUT_BLOCKED        # "llm.guard.input.blocked"
EventType.GUARD_OUTPUT_BLOCKED       # "llm.guard.output.blocked"

# ... and many more

To use your own event types, prefix with x.<company>:

event = Event(
    event_type="x.mycompany.pipeline.completed",
    source="my-tool@1.0.0",
    payload={"result": "ok"},
)

Serialisation

Events serialise to and from plain Python dicts and JSON strings:

# dict (omits None fields by default)
d = event.to_dict()
d = event.to_dict(omit_none=False)   # include all fields

# JSON string
s = event.to_json()

# round-trip
event2 = Event.from_dict(d)
event3 = Event.from_json(s)
assert event == event2 == event3

Validation

Event.validate() raises SchemaValidationError on the first invalid field:

event.validate()   # silent on success; raises on first error

For full JSON Schema validation (requires pip install "spanforge[jsonschema]"):

from spanforge.validate import validate_event
validate_event(event)

Pydantic integration

For tools that use Pydantic v2 (requires pip install "spanforge[pydantic]"):

from spanforge.models import EventModel

model = EventModel.from_event(event)
schema = model.model_json_schema()
event_back = model.to_event()

ULIDs

Event IDs use the ULID format — 26-character, URL-safe, time-ordered, sortable unique identifiers:

from spanforge.ulid import generate, validate, extract_timestamp_ms

ulid = generate()                         # "01JPXXXXXXXXXXXXXXXXXXXXXXXX"
assert validate(ulid) is True
ts_ms = extract_timestamp_ms(ulid)        # milliseconds since epoch