Skip to content
Tracon

RunEventWriter

Namespace Tracon · Assembly Tracon.Core.dll

Writes the events of a single run into IRunStore and produces the sequence number.

public sealed class RunEventWriter
Inheritance
object ← RunEventWriter
Inherited Members
object.GetType(), object.ToString(), object.Equals(object?), object.Equals(object?, object?), object.ReferenceEquals(object?, object?), object.GetHashCode()

The sequence number is produced by a single writer, one event at a time. Numbering an event, writing it to the store and handing it to every IRunEventSink happen as one step, and the next event waits for it. A run writes from more than one place at once (concurrent tool calls, a child run writing into its parent’s writer, a workflow’s executors next to its event pump), so without that the store and the sinks could see event 6 before event 5. This keeps the event order deterministic and guarantees that live streaming and replay produce the same result.

A store or a sink therefore must not call RunEventWriter.AppendAsync on the writer whose event it is handling: that append would wait for the very event it is part of. From a sink, such an append throws InvalidOperationException instead of waiting, and the sink is disabled for the run.

Store failures do not interrupt the run. Observability must not break functionality. Every write failure is logged and swallowed; once the writer hits a failure, it moves into the RunEventWriter.IsDisabled state and attempts no further writes for that run.

The loss is counted, not only logged. Every lost write increments TraconDiagnostics.RunRecordingFailureCounterName, tagged with the tenant and the stage it was lost at, so that a hole in a run’s evidence is something a dashboard shows rather than something a reader of logs eventually notices.

Tracon creates one writer for each run. Code that runs inside a run, such as a tool, reads it from AgentRunScope.Writer of TraconRunContext.Current and appends its own events with RunEventWriter.AppendAsync.

Gets the number of events written.

public long EventCount { get; }

long

Gets whether the writer was disabled because it hit a store failure. A disabled writer silently does nothing.

public bool IsDisabled { get; }

bool

Gets the identity of the run this writer writes to.

public Guid RunId { get; }

Guid

Gets the tenant of the run this writer writes to. Stamped onto every sub-write as defense in depth.

public string? TenantId { get; }

string?

The value is the run’s own tenant, which Tracon stamps when it starts the run (RunStartInfo.TenantId) — not the ambient tenant. It can deliberately override the ambient tenant (this is how workflows and job queues work). A writer whose run was never started stays null and no tenant check is performed.

AppendAsync(RunEventDraft, CancellationToken)

Section titled “ AppendAsync(RunEventDraft, CancellationToken)”

Appends an event to the stream and assigns its sequence number.

public ValueTask<RunEvent> AppendAsync(RunEventDraft draft, CancellationToken cancellationToken = default)

draft RunEventDraft

The event draft.

cancellationToken CancellationToken

The cancellation token.

ValueTask<RunEvent>

The event with its sequence number and timestamp assigned. The event is produced even if the store fails or the writer is disabled.

Returning the event is for workflow execution: the streaming endpoint must both write the same event to the store and send it to the client, and building it a second time would split the sequence number in two. Returning a value from a disabled writer is also deliberate — closing observability must not interrupt the response streaming to the client.