`, or `Unknown` | - |
| `JobProgressEvent` | the payload and an optional percent; `hasProgress()` is true when a percent of zero or more was given | the payload's text form, null for a null payload | no percent `STATUS_UPDATE`, 0 `STARTING`, 1-99 `PROGRESSING`, 100 `COMPLETING`; the title is the job's display name |
| `ContentStreamEvent` | a `StreamChunk` (as `chunk()`) and the artifacts given with it, on any chunk, copied at construction and null when none | `Streaming chunk: `, `Streaming complete` on the last | `PROGRESSING` / `Streaming Content`, then `COMPLETING` / `Streaming Complete` at 100% on the last chunk; the human message is the chunk's content, else `Streaming...` or `Stream completed` |
| `UserNotificationEvent` | title, notification, severity, job state, optional details; the four factories read the state off the snapshot and carry no details | the notification | `ERROR` for an error, `COMPLETING` for a success, `MESSAGE` for a warning or an info; the title as given |
| `OrchestratorIdleEvent` | the exchange's LLM responses and metadata, as given | `Ready` | - |
| `OrchestratorResumedEvent` | nothing beyond the snapshot | `Processing` | - |
| `WorkflowCompleteEvent` | success and reason; the one-argument form is a success reading `Workflow completed successfully` | the reason | `COMPLETING` / `Workflow Complete` or `FAILING` / `Workflow Failed` |
| `MessageCompleteEvent` | success and an error description on failure | `Message processed` or `Message processing failed` | `COMPLETING` / `Message Complete` or `FAILING` / `Message Failed` |
| `SchedulerEvent` | a type (`STARTED`, `STOPPING`, `STOPPED`, `ERROR`) and, for an error, its cause | the type's default message unless a caller gave one; `Scheduler error: ` (or `Unknown`) for an error | - |
| `LimiterEvent` | one admission transition: account name and category, capacity, in-use, waiters, wait, reject reason, status indicator, amount | ` HELD (/, waiters=)`, ` GRANTED (/)`, ` GRANTED after ms`, ` REJECTED ()`, ` RELEASED (/)` | - |
`EventMessageContractTest` holds every row of this table; the queued job's cancellation message is held by `KernelExecutionContractTest.aJobTakenOutOfAQueueIsSettledAsCancelled` in `harness`, and the immutability by `EventCategoryContractTest.noEventExposesAPublicMutator`. The retry and heartbeat tables are in their own packages.
### The terminal family
`AbstractTerminalEvent` is the base of the four events the dispatcher fires for a job it ran: completion, failure, timeout and cancellation. Each carries the attempt count, the LLM responses and the metadata collected during the run (an empty list or map when none were given, never null), and the instant the terminal state was reached, which is `getCompletionTime()`; `getDuration()` runs from the snapshot's start to that instant and is zero for a job that never started. `getResult()` is present on a completion only; the error is read through `getError()` and `getCause()`. `getCause()` is the failure's error on a `FailureEvent` and empty otherwise, so a cancellation has none. `DependencyFailureEvent` is the one terminal event outside the base class: the job never ran.
## Delivery scope
A subscription to a sealed category receives every member of it and nothing outside it: a `LifecycleEvent` subscriber sees a start and a completion, never a progress event and never a `WorkflowCompleteEvent`, which is parallel to lifecycle. An event with no snapshot reaches a global subscription (`Job.class`, `JobEvent.class`, no workflow) and a by-type subscription for all jobs (`Job.class` and the event's type or category, which is how a `Stethoscope` hears every `HeartbeatEvent`), never a subscription scoped to a job type or to a workflow: those never see a `SchedulerEvent`, a heartbeat event or a job-less `LimiterEvent`. `EventDeliveryScopeTest` holds both rules on the real bus.
## Built-in observers
| Observer | Scope | Subscribes to | Purpose |
|----------|-------|---------------|---------|
| `EventLogger` | Global | `JobEvent` (excludes `OperationalEvent` in its predicate) | Color-coded console log of all job events |
| `SystemHealthReporter` | Global | `JobEvent` (filter: `LimiterEvent` + `LifecycleEvent`) | Sampled system snapshot: limiters sorted by utilisation with gradient bar and status column, running jobs by type, active workflows, queue depths plus the count of jobs parked in admission. Red/yellow row tinting for blocked or throttled limiters. For `token_bucket` limiters the `used:` column is the rolling-minute sum of `LimiterEvent.amount` over `GRANTED_*` events (true throughput against the per-minute capacity), since `inUse` for token buckets is bucket depletion and would read near-empty under sustained pressure. Other categories display `inUse` directly. |
| `StreamingLogObserver` | Per-workflow | `JobEvent` | Streaming console output with reasoning/tool boxes |
| `OpenTelemetryObserver` | Global | `JobEvent` | Translates events into OTel spans (optional dep) |
| `MicrometerObserver` | Global | `JobEvent` | Forwards events to a Micrometer registry (optional dep) |
A deployment that persists job runs adds its own `LifecycleEvent` subscriber; none ships with the runtime.
---
# Inside heartbeat events
Part 13 · Inside the runtime
This page is for people working on the runtime itself: why the heartbeat events are a
category of their own, what each one carries and renders as its message, and the tests that
hold them. To schedule and watch heartbeats, read [the guide](events-heartbeat.html) first.
## The category
`HeartbeatEvent` is sealed to six members. It is deliberately not `OperationalEvent`: that
category is high-volume telemetry broad subscribers skip, while these are low-volume commands
and audit-worthy transitions dashboards and persisters subscribe to. Every member answers a
null snapshot, because a heartbeat transition has no owning job; an event with no snapshot
reaches global and by-type subscriptions (a `Stethoscope` subscribes to `HeartbeatEvent` for
all jobs), never a job-type-scoped or a workflow-scoped one. The fired
job's own lifecycle events carry its snapshot; the correlation from a schedule to its runs is
the `jobId` on `HeartbeatFired`.
| Event | Published by | Carries | Message |
|---|---|---|---|
| `HeartbeatRequested` | the job context, for a `ScheduleHeartbeat` an orchestrator published | the minted `Heartbeat`; refuses to exist without one, since the heartbeat is what carries the captured authority | `Heartbeat requested: ` |
| `CancelHeartbeat` | any job, through its context | the `heartbeatId` to remove; a null or blank id is refused, there is no cancel-all; an in-flight fired job is untouched | `Heartbeat cancel requested: ` |
| `HeartbeatScheduled` | the Heart, when a request landed in the store | the heartbeat | `Heartbeat scheduled: ` |
| `HeartbeatRebound` | the Heart, when a request re-used an existing id: most recent wins | the displaced spec and the current one | `Heartbeat rebound: (was , now )` |
| `HeartbeatFired` | the Heart, when a due heartbeat was dispatched | the heartbeat and the run's `jobId` | `Heartbeat fired: -> job ` |
| `HeartbeatFireFailed` | the Heart, when submission threw | the heartbeat and the failure; a recurring entry's next instance is enqueued regardless | `Heartbeat fire failed: - ` |
Each event is stamped with its construction instant, and each carries the instant explicitly
so a record built from a stored transition keeps its own.
`HeartbeatEventContractTest` holds the two requests; `HeartbeatDispatchTest` in
`harness` drives the four notifications through the Heart and pins their messages.
---
# Inside retry events
Part 13 · Inside the runtime
This page is for people working on the runtime itself: what the retry events have in common,
the exact messages and titles they render, the test that holds them, and where the dispatcher
builds each one. To show retries to a person, read [the guide](events-retry.html) first.
## The category
`RetryEvent` is sealed to four members. Every member is `HumanReadable`, carries the job's
snapshot, renders its message from what it carries, answers `STATUS_UPDATE` as its `MsgType`
rather than a lifecycle phase, and names itself in its title.
| Event | Published for | Carries | Message |
|---|---|---|---|
| `RateLimitRetryEvent` | a 429, the framework's own budget | the limit type, the model, the attempt, the system throttle and the estimated delay | `Hit acceleration limit (system throttle: <1+throttle>x) for . Retrying in ~ seconds (attempt )` for an acceleration limit; `Hit capacity limit for . ...` for any other kind |
| `TransientErrorRetryEvent` | a transient server error, a plain 5xx or a 529 overload; the error details name which | the provider, its words, the attempt and the estimated delay | `Server error from : . Retrying in ~ seconds (attempt )` |
| `OutputTruncationRetryEvent` | a reply stopped at `max_tokens`, re-run with a larger budget | the model, the previous budget and the new one | `Output truncated at tokens on . Retrying with budget .` |
| `ResponseCorrectionRetryEvent` | a reply that failed to parse or validate, re-run with a correction appended | the model, the correction attempt and the failure's class name | `Response on