Summary
WebAgentEventData declares timestamp: number, but the events emitted in a run's
result JSON do not carry it. The wrapper timestamp that is present is an ISO
string assigned in clusters, so it cannot be used to time individual agent steps —
while still looking precise enough that a consumer will trust it.
What the data model says
packages/core/src/events.ts:
export interface WebAgentEventData {
timestamp: number;
}
Every event data interface extends this — AgentStepEventData,
ReasoningEventData, ActionExecutionEventData, and the rest. So a numeric,
per-event epoch timestamp appears to be the intended contract.
What a run actually emits
From a single CLI run (95 events):
- The
data objects carry no timestamp:
agent:step → { currentIteration, iterationId }
agent:action → { action, value }
agent:reasoned → { iterationId, reasoning }
- The wrapper
timestamp is an ISO string, and the values cluster: 95 events
resolve to only 28 distinct timestamps. Three consecutive agent:step
events shared one millisecond (…T22:51:49.719Z), so the timestamps look like
they are assigned at serialisation time rather than when each event occurred.
The one event that works
task:metrics_incremental carries real epoch milliseconds and is monotonic, because
packages/core/src/loggers/metricsCollector.ts sets timestamp: Date.now()
explicitly. Across two runs it was exactly 1:1 with agent:step and with
metadata.stepCount (4/4/4 and 10/10/10), with plausible per-step deltas
(5.8s, 3.1s, 2.2s, 3.5s, 4.7s, 4.3s, 5.3s, 2.2s, 3.5s).
So usable per-step timing is recoverable today — but only from that one event
type, and only by knowing to prefer it over the timestamps on every other event.
Why it's worth fixing
Two traps for consumers:
- The ISO timestamps are plausible and wrong. Anyone building a timeline from
agent events will produce something that renders fine and misrepresents when
things happened. Missing data announces itself; clustered data does not.
- Timestamp types are mixed within one array.
task:metrics_incremental's
timestamp is a number; every other event's is a string. A single
Date.parse() pass over the array silently yields NaN for the only entries
that carry real timing.
Not verified
I did not trace the collector that assembles the result JSON's events array and
assigns the outer ISO timestamp — that's presumably where the declared
data.timestamp is either never populated or overwritten. Someone closer to that
code can confirm the mechanism; the observable symptoms above are what I measured.
Suggested direction
Populate data.timestamp with Date.now() at the point each event is created, as
metricsCollector already does, and leave the wrapper timestamp alone (or derive it
from the event's own value). That makes the declared contract true and removes the
need for consumers to special-case one event type.
Summary
WebAgentEventDatadeclarestimestamp: number, but the events emitted in a run'sresult JSON do not carry it. The wrapper timestamp that is present is an ISO
string assigned in clusters, so it cannot be used to time individual agent steps —
while still looking precise enough that a consumer will trust it.
What the data model says
packages/core/src/events.ts:Every event data interface extends this —
AgentStepEventData,ReasoningEventData,ActionExecutionEventData, and the rest. So a numeric,per-event epoch timestamp appears to be the intended contract.
What a run actually emits
From a single CLI run (95 events):
dataobjects carry notimestamp:agent:step→{ currentIteration, iterationId }agent:action→{ action, value }agent:reasoned→{ iterationId, reasoning }timestampis an ISO string, and the values cluster: 95 eventsresolve to only 28 distinct timestamps. Three consecutive
agent:stepevents shared one millisecond (
…T22:51:49.719Z), so the timestamps look likethey are assigned at serialisation time rather than when each event occurred.
The one event that works
task:metrics_incrementalcarries real epoch milliseconds and is monotonic, becausepackages/core/src/loggers/metricsCollector.tssetstimestamp: Date.now()explicitly. Across two runs it was exactly 1:1 with
agent:stepand withmetadata.stepCount(4/4/4 and 10/10/10), with plausible per-step deltas(5.8s, 3.1s, 2.2s, 3.5s, 4.7s, 4.3s, 5.3s, 2.2s, 3.5s).
So usable per-step timing is recoverable today — but only from that one event
type, and only by knowing to prefer it over the timestamps on every other event.
Why it's worth fixing
Two traps for consumers:
agent events will produce something that renders fine and misrepresents when
things happened. Missing data announces itself; clustered data does not.
task:metrics_incremental'stimestampis anumber; every other event's is astring. A singleDate.parse()pass over the array silently yieldsNaNfor the only entriesthat carry real timing.
Not verified
I did not trace the collector that assembles the result JSON's
eventsarray andassigns the outer ISO timestamp — that's presumably where the declared
data.timestampis either never populated or overwritten. Someone closer to thatcode can confirm the mechanism; the observable symptoms above are what I measured.
Suggested direction
Populate
data.timestampwithDate.now()at the point each event is created, asmetricsCollectoralready does, and leave the wrapper timestamp alone (or derive itfrom the event's own value). That makes the declared contract true and removes the
need for consumers to special-case one event type.