Skip to content

aep/emitter: seeding historical AEP records requires threading timestamp_ms through every addAction #19

Description

@telleroutlook

Seeding historical AEP records is awkward — no ergonomic timestamp override

Repo: packages/aep

Problem

We wanted to seed demo AEP data at service startup so integrators / demos see a realistic audit trail without first running the agent. The natural pattern is "make it look like this run happened 3 days ago at 23:17". Doing that today requires:

  1. Passing timestamp_ms into every addAction() call
  2. Passing createdAtMs into build() / emit()
  3. Manipulating the private #actions array is impossible (readonly private field), so you can't fix timestamps post-hoc

There's no AEPEmitter.forRun(runStartMs, opts) or emitter.setStartTime(ms) helper.

Evidence

packages/aep/src/emitter.ts:

Constructoremitter.ts:38-40. AEPEmitterOptions (lines 15-27) has no created_at_ms / start_ms field. Can't be set once and reused.

build()emitter.ts:93, signature build(createdAtMs?: number): AEPRecord. So per-call override exists, but requires re-passing the same value on every call.

addAction()emitter.ts:42-61:

addAction(
  action: Omit<ActionEvidence, "action_id" | "timestamp_ms"> & {
    action_id?: string;
    timestamp_ms?: number;
  }
): void {
  this.#actions.push({
    action_id: action.action_id ?? `action-${this.#actions.length}`,
    timestamp_ms: action.timestamp_ms ?? Date.now(),
    ...
  });
  ...
}

Each timestamp_ms must be supplied explicitly. Default is Date.now() — perfect at runtime, unhelpful for seeding.

#actions is private+readonlyemitter.ts:31: readonly #actions: ActionEvidence[] = []. So even if the caller wanted to walk it and adjust timestamps after adding, they can't.

Reproducer

Seeding a demo record for "3 days ago, 4 actions at 23:17 + 1s each":

// Current API — the caller has to compute each timestamp manually
const baseMs = threeDaysAgoAtLocal2317();
const e = new AEPEmitter({ run_id: 'demo-001', model_id: 'x' });
e.addAction({ tool_name: 'a', state_changing: false, timestamp_ms: baseMs + 1000 });
e.addAction({ tool_name: 'b', state_changing: false, timestamp_ms: baseMs + 2000 });
e.addAction({ tool_name: 'c', state_changing: true,  timestamp_ms: baseMs + 3000 });
e.addAction({ tool_name: 'd', state_changing: true,  timestamp_ms: baseMs + 4000 });
const rec = e.build(baseMs);   // remember to pass createdAtMs, again

Every seed call carries the baseMs plumbing. In our project we seed 10 records covering multiple scenarios — the code got noisier than the actual scenario definitions.

What we ended up doing

Since we couldn't set the clock on the emitter, we ported it to Python and gave our Python AEPEmitter a private field we overwrite:

e = AEPEmitter(run_id=f'demo-seed-{i:03d}', ...)
e._created_at_ms = run_ts_ms  # private field — feels hacky
e._start_ms = run_ts_ms

We'd rather do this via the public API of the TS emitter (since we'll eventually converge on it) but there isn't one.

Proposed fix

Simple option: add an AEPEmitterOptions.created_at_ms?: number and a now?: () => number clock injection:

export interface AEPEmitterOptions {
  // ... existing fields
  /** Fixed run start; if provided, build()/emit() use this by default. */
  created_at_ms?: number;
  /** Clock injection for addAction default timestamps and for build() default. */
  now?: () => number;
}

Then:

const t0 = threeDaysAgoAtLocal2317();
let t = t0;
const e = new AEPEmitter({
  run_id: 'demo-001',
  created_at_ms: t0,
  now: () => (t += 1000),   // each addAction advances by 1s
});
e.addAction({ tool_name: 'a', state_changing: false });
e.addAction({ tool_name: 'b', state_changing: false });
// ...
const rec = e.build();  // uses created_at_ms from options

Cleaner for seeding, cleaner for tests, no impact on the runtime path (default now is Date.now).

Bonus: this makes the emitter easier to unit-test — deterministic clock.

Happy to send a PR.


Filed by: CATL Ariba Joule integration team. We seed 10 demo AEP records at service startup for the audit tab; the current API forced us to duplicate the clock plumbing in every scenario definition.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions