Skip to content

FE-763: Petrinaut event stream — initial markings + transition firings#158

Open
kostandinang wants to merge 1 commit into
ka/fe-762-petri-blueprint-exportfrom
ka/fe-763-petri-event-stream
Open

FE-763: Petrinaut event stream — initial markings + transition firings#158
kostandinang wants to merge 1 commit into
ka/fe-762-petri-blueprint-exportfrom
ka/fe-763-petri-event-stream

Conversation

@kostandinang
Copy link
Copy Markdown
Contributor

@kostandinang kostandinang commented May 27, 2026

Summary

  • New module petrinaut-events.tscreatePetrinautEventStream({ runId, filePath?, tokenIdFn?, onEvent? }) returns a NetEventSink adapter plus emitInitialMarking(blueprint) helper. Writes one JSON object per line to filePath when provided and fans out to onEvent for in-process consumers.
  • Cook writes <runDir>/petrinaut-events.jsonl on every run: initial_marking up-front, then transition_fired per fire, then any net_halted / net_deadlocked terminal event.
  • Wire format matches the cross-team agreement (2026-05-26): tokens are { id: <UUID>, ...payload }; input / output are Record<place, tokens[]>; every event carries runId.

Context

  • Pairs with FE-762 (blueprint export) — together they give Petrinaut everything it needs to visualize a live cook run. Blueprint is the static topology; this PR is the runtime overlay.
  • Stacks on FE-762 because both writers extend engine.ts with input.runDir-conditional behavior; chaining them avoids a self-conflict in that file. Stack ordering is a Graphite concern; the underlying frontier items remain "parallel" per memory/PLAN.md.
  • Decouples visualization from reports.jsonl — Petrinaut consumes the new stream directly; reports stay the orchestrator's internal log.
  • Halt outcomes (FE-761 Slice 2b halted-as-place) appear naturally as halt tokens landing on slice:<sid>:halted / epic:<eid>:halted places via transition_fired events, plus a terminal net_halted from the engine.

What changed

  • New src/orchestrator/src/petrinaut-events.ts: pure adapter from the orchestrator's NetEvent stream to Petrinaut's wire format.
    • Types: PetrinautEvent discriminated union over initial_marking / transition_fired / net_halted / net_deadlocked; PetrinautToken { id, sliceId?, epicId?, retryCount?, reworkCount?, haltReason? }.
    • emitInitialMarking(blueprint) derives the up-front marking from blueprint.initialTokens, assigns fresh UUIDs, and groups by place.
    • sink.emit(event) translates each NetEvent into the corresponding Petrinaut event, building input / output records from the parallel consumedTokens / producedTokens arrays.
  • petri-net.ts: NetEvent gains parallel optional consumedTokens?: Token[][] and producedTokens?: Token[][] arrays — populated for transition_fired so the adapter can include per-arc token payload without re-reading the net. scheduleDeferred gains a consumedTokens parameter alongside consumedPlaces so async fires emit the same shape as sync fires.
  • net-compiler.ts: all four scheduleDeferred call sites pass the captured consumed tokens through.
  • engine.ts: when input.runDir is present, opens a Petrinaut event stream writing to <runDir>/petrinaut-events.jsonl, emits initial_marking from the compiled blueprint, then passes the sink to net.run().

Verification

  • 4 new unit tests in petrinaut-events.test.ts: initial_marking grouping, transition_fired adapter shape, terminal-event forwarding, JSONL file roundtrip via mkdtempSync + readFileSync.
  • 1 new end-to-end test in engine-contract.test.ts running simplePlan happy path with the Petrinaut sink — asserts initial_marking first, runId on every event, the FE-761 Slice 4 dispatch + complete transition names both appear in the stream, every token carries an id, and happy paths emit no net_halted / net_deadlocked.
  • All 130 orchestrator tests pass; full npm run verify (check + test + build) green.
  • Outer-loop end-to-end replay against Petrinaut's live consumer deferred to FE-764 transport work and cross-team validation.

Out of scope

  • Transport / sync server — wiring the event stream to a live Petrinaut session (FE-764).
  • Token UUID lifecycle across consume → emit (lineage tracing) — v1 generates fresh UUIDs per emission; pending Petrinaut team confirmation on whether identity should persist.
  • Petrinaut blueprint export — sibling sub-issue (FE-762), already in the stack.
  • Final JSON envelope shape per Petrinaut's loader — v1 best-guess; expect cross-team revisions once Petrinaut publishes their schema.

Traceability

  • Linear FE-763 (parent FE-760); pairs with FE-762 and feeds FE-764.
  • Frontier petri-event-stream in memory/PLAN.md; stacks on FE-762 (which stacks on FE-761).

@kostandinang kostandinang force-pushed the ka/fe-762-petri-blueprint-export branch from dc53d4c to f6356f7 Compare May 27, 2026 23:16
@kostandinang kostandinang force-pushed the ka/fe-763-petri-event-stream branch from 0d89acd to 7590a6f Compare May 27, 2026 23:16
@kostandinang kostandinang marked this pull request as ready for review May 27, 2026 23:17
@cursor
Copy link
Copy Markdown

cursor Bot commented May 27, 2026

PR Summary

Low Risk
Observability-only path gated on runDir; core firing semantics unchanged aside from richer event metadata for adapters.

Overview
Adds a Petrinaut runtime event stream so live cook runs can be visualized alongside the static blueprint (FE-762).

A new createPetrinautEventStream adapter turns internal NetEvents into the agreed wire shape: initial_marking (from blueprint seeds), transition_fired with per-place input/output tokens ({ id, …payload }), and net_halted / net_deadlocked. When input.runDir is set, the engine writes <runDir>/petrinaut-events.jsonl, emits initial marking before net.run, and passes the sink through; callers without runDir stay unchanged.

The Petri interpreter now attaches consumedTokens / producedTokens on transition_fired (sync and deferred completions), and scheduleDeferred takes the consumed token list so async handler completions show the same payload as synchronous fires.

Reviewed by Cursor Bugbot for commit 9c0641c. Bugbot is set up for automated code reviews on this repo. Configure here.

@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented May 27, 2026

🤖 Augment PR Summary

Summary: Adds a Petrinaut-focused runtime event stream to the orchestrator so Petrinaut can visualize live cook runs (initial marking + per-transition firings + terminal outcomes).

Changes:

  • Introduced createPetrinautEventStream() (petrinaut-events.ts) that adapts internal NetEvents into the cross-team JSONL wire format and can optionally write petrinaut-events.jsonl.
  • Added emitInitialMarking(blueprint) helper to emit a single up-front initial_marking event derived from the compiled blueprint.
  • Extended NetEvent to include per-arc consumedTokens/producedTokens so downstream adapters can include token payloads without re-reading net state.
  • Threaded consumed token data through deferred transition completion (scheduleDeferred) and through net-compiler call sites.
  • When input.runDir is present, the orchestrator now opens the Petrinaut JSONL stream, emits initial_marking, and passes the sink into net.run().
  • Added unit tests for adapter shape + JSONL roundtrip, plus an end-to-end contract test asserting ordering and per-event runId.

Technical Notes: The stream writes one JSON object per line (JSONL) and fans out to an optional in-process onEvent callback; token IDs are generated per emission (lineage persistence is explicitly deferred).

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

kind: 'transition_fired',
ts: event.ts,
runId,
transitionName: event.transitionId ?? '',
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transitionName: event.transitionId ?? '' will silently emit an empty transition name if an upstream transition_fired event is malformed; that can hide bugs and may violate the Petrinaut schema. Consider failing fast (or otherwise making missing transitionId unmistakable) for transition_fired events.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

tokens: Token[][] | undefined,
): Record<string, PetrinautToken[]> {
const out: Record<string, PetrinautToken[]> = {};
if (!places || !tokens) return out;
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

groupTokens() returns {} when places is present but tokens is missing, which would silently drop arc/place information from emitted Petrinaut events. Consider validating places/tokens alignment and/or emitting empty arrays per place so data loss is obvious.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

// ---------------------------------------------------------------------------

describe('FE-763: Petrinaut event stream on a real run', () => {
it('emits initial_marking + transition_fired (with token payload) + net_halted for simplePlan happy path', async () => {
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test description mentions net_halted, but the assertions below expect zero net_halted/net_deadlocked events on the happy path; consider updating the description to match the intended behavior.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Emits the runtime events Petrinaut needs to visualize a live cook run, in
the cross-team-agreed payload shape (2026-05-26 alignment):

  initial_marking:
    { kind, ts, runId, marking: { <place>: [{id, ...payload}] } }

  transition_fired:
    { kind, ts, runId, transitionName,
      input:  { <place>: [{id, ...payload}] },
      output: { <place>: [{id, ...payload}] } }

  net_halted / net_deadlocked:
    { kind, ts, runId }

What landed:

- New module src/orchestrator/src/petrinaut-events.ts: pure adapter
  createPetrinautEventStream({ runId, filePath?, tokenIdFn?, onEvent? })
  returning { sink: NetEventSink, emitInitialMarking(blueprint) }.
  Writes one JSONL object per line to filePath when set and fans out to
  onEvent for in-process consumers (tests, future sync-server forwarder).

- petri-net.ts: NetEvent gains parallel optional consumedTokens?: Token[][]
  and producedTokens?: Token[][] (one entry per arc, same indexing as the
  existing consumed/produced place-name lists). These are populated for
  transition_fired events so the adapter can render the per-place
  { id, ...payload } shape Petrinaut expects without re-reading the net.
  scheduleDeferred gains a consumedTokens parameter alongside the existing
  consumedPlaces so async fires emit the same shape as sync fires.

- net-compiler.ts: all four scheduleDeferred call sites pass the captured
  consumed tokens through to the deferred event.

- engine.ts: when input.runDir is present, opens a Petrinaut event stream
  writing to <runDir>/petrinaut-events.jsonl, emits initial_marking from
  the compiled blueprint up-front, then passes the sink to net.run(). The
  same gate that drives FE-762's <runDir>/net.json write — library callers
  without a runDir get the existing no-op behavior.

Halt outcomes (FE-761 Slice 2b halted-as-place):
- Petrinaut sees halt as a halt token traveling through the topology via
  transition_fired events landing on slice:<sid>:halted / epic:<eid>:halted,
  plus a terminal net_halted event from the engine.

Open coordination item: token UUID lifecycle across consume->emit (lineage
tracing). v1 generates fresh UUIDs per emission. When Petrinaut decides
whether identity should persist, this module is the seam to evolve.

Tests:
- 4 unit tests in petrinaut-events.test.ts covering initial_marking,
  transition_fired adapter shape, terminal events, and JSONL file
  roundtrip via mkdtempSync.
- 1 end-to-end test in engine-contract.test.ts running simplePlan happy
  path with the Petrinaut sink — asserts initial_marking first, runId on
  every event, the FE-761 Slice 4 dispatch + complete transition names
  both appear, every token carries an id, and happy paths emit no
  net_halted / net_deadlocked.

All 130 orchestrator tests pass; npm run fix + check + build all green.

Co-authored-by: Amp <amp@ampcode.com>
@kostandinang kostandinang force-pushed the ka/fe-762-petri-blueprint-export branch from f6356f7 to ff3504c Compare May 27, 2026 23:21
@kostandinang kostandinang force-pushed the ka/fe-763-petri-event-stream branch from 7590a6f to 9c0641c Compare May 27, 2026 23:21
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9c0641c. Configure here.

consumed: transition.inputs,
consumedTokens: consumed.map((t) => [t]),
produced: producedPlaces,
producedTokens,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred handlers emit duplicate transition_fired events with consumed tokens

Medium Severity

For deferred fire handlers (action, run-tests, assess-semantic, verify-epic), depositClaim emits a transition_fired event with populated consumedTokens but empty producedTokens when the fire handler returns []. Then completeDeferred emits a second transition_fired for the same transition with the same consumedTokens plus the actual producedTokens. The rename from _consumed to consumed and the addition of consumedTokens: consumed.map((t) => [t]) means the Petrinaut stream now receives two rich events per deferred firing — the first showing tokens vanishing with no output, the second claiming the same tokens were consumed again. A Petrinaut visualizer animating token movements would see contradictory state.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9c0641c. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant