orchestrata_ultimate.kernel.pipeline_events
Reference › orchestrata_ultimate › kernel › pipeline_events
The milestone event stream of a pipeline run. Whenever the runner boots, takes an intent, escalates between volks, runs a review or tests, or finishes, it emits a PipelineEvent so that a UI or CLI can watch the run live. The module serves SPEC-001 story US-006 (REQ-001-US-006-020). It is imported by pipeline, pipeline_escalation, pipeline_session, the streaming client cli_watch, the gateway server and orchestrator.pipeline_kernel. It is separate from the system-wide event_bus.
PipelineEventType is a StrEnum that is the single source of truth for the taxonomy: boot, intent, escalation, swarm start, shard and done, review, test result, complete, stage, paused, resumed and cancelled, each a pipeline.-prefixed string, so existing string comparisons keep working. PipelineEvent carries the type, thread id, volk, iteration, timestamp, data and a driver identity; to_payload serialises it. The build_*_event functions construct the common events, and pipeline_topic returns the per-thread UI topic name.
PipelineEventBus.emit writes the JSON event to a Redis list, trimmed to a maximum length, forwards it to the UI, and calls the registered callbacks (added with on). The driver field is filled from the bus's own identity (inprocess by default) on the serialised copy only, and a producer-supplied driver is kept. _forward_to_ui adds the event to two Redis streams the gateway already reads: a per-thread ring buffer, so reconnecting clients can replay, and a global stream the gateway forwarder tails to broadcast onto the thread topic. Because the kernel and gateway share Redis, this works across processes. Without a Redis client, events only reach the log and the callbacks. Redis and callback errors are logged and never raised. The emit_* helpers (emit_boot, emit_intent, emit_escalation, emit_review, emit_test_result, emit_complete and others) wrap this for each event type.
At a glance
| Source | src/orchestrata_ultimate/kernel/pipeline_events.py |
| Lines of code | 595 |
| Classes | 3 |
| Functions | 7 |
| Imports (internal) | orchestrata_ultimate.core.base.traces |
| Imported by | orchestrata_ultimate.gateway.server, orchestrata_ultimate.kernel.cli_watch, orchestrata_ultimate.kernel.pipeline, orchestrata_ultimate.kernel.pipeline_escalation, orchestrata_ultimate.kernel.pipeline_session, orchestrata_ultimate.orchestrator.pipeline_kernel |
| Tests that import it | tests/gateway/test_pipeline_forwarder_canonical_shape.py, tests/kernel/test_cli_watch.py, tests/kernel/test_pipeline_artifacts.py, tests/kernel/test_pipeline_checkpoint.py, tests/kernel/test_pipeline_compliance_failed.py, tests/kernel/test_pipeline_escalation.py, tests/kernel/test_pipeline_event_forwarder.py, tests/kernel/test_pipeline_events.py, tests/kernel/test_pipeline_events_enum.py, tests/kernel/test_pipeline_session.py, tests/kernel/test_pipeline_session_profile_root.py, tests/orchestrator/test_pipeline_kernel.py, tests/orchestrator/test_pipeline_kernel_workflow.py, tests/test_pipeline_events.py |
Class diagram
Dependency diagram
API
Classes
PipelineEventType (enum)
Bases: StrEnum · Decorators: traces(spec='001', story='US-006', reqs=['REQ-006-031', 'REQ-006-032', 'REQ-006-033']) · Line 38
The pipeline event taxonomy — the single source of truth.
| Member | Value |
|---|---|
BOOT |
'pipeline.boot' |
INTENT |
'pipeline.intent' |
ESCALATION |
'pipeline.escalation' |
SWARM_START |
'pipeline.swarm.start' |
SWARM_SHARD |
'pipeline.swarm.shard' |
SWARM_DONE |
'pipeline.swarm.done' |
REVIEW |
'pipeline.review' |
TEST_RESULT |
'pipeline.test.result' |
COMPLETE |
'pipeline.complete' |
STAGE |
'pipeline.stage' |
PAUSED |
'pipeline.paused' |
RESUMED |
'pipeline.resumed' |
CANCELLED |
'pipeline.cancelled' |
PipelineEvent (dataclass)
Decorators: traces(spec='001', story='US-006', reqs=['REQ-001-US-006-020']), dataclass · Line 86
A milestone event in the pipeline lifecycle.
| Attribute | Type | Default |
|---|---|---|
event_type |
PipelineEventType |
|
thread_id |
str |
'' |
volk |
str |
'' |
data |
dict[str, Any] |
field(default_factory=dict) |
timestamp |
str |
field(default_factory=lambda: datetime.now(UTC).isoformat()) |
iteration |
int |
0 |
driver |
str |
'' |
| Method | Signature | Summary |
|---|---|---|
to_payload |
to_payload() -> dict[str, Any] |
The canonical broadcast serialization for this event. |
PipelineEventBus
Line 201
Publishes pipeline events to Redis and optional callbacks.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(redis_client: Any | None = None, events_key: str = EVENTS_KEY, ui_stream_key: str | None = None, driver: str = 'inprocess') -> None |
Initialize the event bus. |
on |
on(callback: Callable[[PipelineEvent], None]) -> None |
Register a callback for all events. |
emit |
emit(event: PipelineEvent) -> None |
Publish an event to Redis and all callbacks. |
_forward_to_ui |
_forward_to_ui(event: PipelineEvent, event_dict: dict[str, Any]) -> None |
Bridge an event to the gateway's UI plumbing (Redis streams). |
emit_boot |
emit_boot(volks: list[str], thread_id: str = '') -> None |
Emit pipeline.boot event. |
emit_intent |
emit_intent(thread_id: str, intent: str) -> None |
Emit pipeline.intent event. |
emit_escalation |
emit_escalation(thread_id: str, from_volk: str, to_volk: str, subject: str) -> None |
Emit pipeline.escalation event. |
emit_swarm_start |
emit_swarm_start(thread_id: str, shard_count: int) -> None |
Emit pipeline.swarm.start event. |
emit_swarm_shard |
emit_swarm_shard(thread_id: str, shard_id: int, file_path: str, success: bool) -> None |
Emit pipeline.swarm.shard event. |
emit_swarm_done |
emit_swarm_done(thread_id: str, succeeded: int, total: int) -> None |
Emit pipeline.swarm.done event. |
emit_review |
emit_review(thread_id: str, volk: str, review_type: str, passed: bool, cycle: int = 0, issues: int = 0) -> None |
Emit pipeline.review event. |
emit_test_result |
emit_test_result(thread_id: str, passed: bool, pass_count: int, fail_count: int, coverage_pct: float = 0.0, duration_s: float = 0.0) -> None |
Emit pipeline.test.result event. |
emit_complete |
emit_complete(thread_id: str, success: bool, reason: str = '') -> None |
Emit pipeline.complete event. |
emit_stage |
emit_stage(thread_id: str, stage: str, status: str, duration_ms: float, artifact_count: int, error: str | None = None) -> None |
Emit pipeline.stage event — a pipeline stage's result. |
emit_paused |
emit_paused(thread_id: str) -> None |
Emit pipeline.paused event. |
emit_resumed |
emit_resumed(thread_id: str) -> None |
Emit pipeline.resumed event. |
emit_cancelled |
emit_cancelled(thread_id: str) -> None |
Emit pipeline.cancelled event. |
Functions
| Function | Signature | Summary |
|---|---|---|
pipeline_topic |
pipeline_topic(thread_id: str) -> str |
Return the UI pub/sub topic for a pipeline thread. |
build_intent_event traces |
build_intent_event(thread_id: str, intent: str, volk: str = '') -> PipelineEvent |
Build a PipelineEventType.INTENT event — the single-source data shape shared by the kernel bus (emit_intent) and the fallback producer's legacy pipeline.start fold (volk="U:Cx"). |
build_stage_event traces |
build_stage_event(thread_id: str, stage: str, status: str, duration_ms: float, artifact_count: int, error: str | None = None) -> PipelineEvent |
Build a PipelineEventType.STAGE event — one pipeline stage's result. volk is the stage alias itself (the stage IS the actor). |
build_complete_event traces |
build_complete_event(thread_id: str, success: bool, reason: str = '', extra: dict[str, Any] | None = None) -> PipelineEvent |
Build a PipelineEventType.COMPLETE event. extra merges additional data fields (e.g. stages_completed/total_artifacts from the legacy pipeline.complete fold, or error from the… |
build_paused_event traces |
build_paused_event(thread_id: str) -> PipelineEvent |
Build a PipelineEventType.PAUSED event. |
build_resumed_event traces |
build_resumed_event(thread_id: str) -> PipelineEvent |
Build a PipelineEventType.RESUMED event. |
build_cancelled_event traces |
build_cancelled_event(thread_id: str) -> PipelineEvent |
Build a PipelineEventType.CANCELLED event. |
Source
# mypy: ignore-errors
"""Pipeline milestone events — emitted at each stage transition.
Events are published to Redis (orchestrata:pipeline:events) and
optionally to the graph for persistent audit trail. External consumers
(goal_runner, dashboards, hooks) subscribe to detect milestones
without parsing conversation.md.
Event types follow the pipeline lifecycle (see ``PipelineEventType`` for the
authoritative, enumerable source of truth):
pipeline.boot — kernel booted, volks registered
pipeline.intent — intent submitted, thread created
pipeline.escalation — volk-to-volk handoff
pipeline.swarm.start — swarm fan-out begins
pipeline.swarm.shard — individual shard completed
pipeline.swarm.done — all shards completed
pipeline.review — self-review or deterministic review result
pipeline.test.result — test results (pass/fail/count/coverage)
pipeline.complete — pipeline finished (success or failure)
pipeline.stage — a pipeline stage finished (status/duration/artifacts)
pipeline.paused — pipeline execution paused
pipeline.resumed — pipeline execution resumed
pipeline.cancelled — pipeline execution cancelled
"""
import json
import logging
from collections.abc import Callable
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any
from orchestrata_ultimate.core.base.traces import traces
@traces(spec="001", story="US-006", reqs=["REQ-006-031", "REQ-006-032", "REQ-006-033"])
class PipelineEventType(StrEnum):
"""The pipeline event taxonomy — the single source of truth.
Members equal the pre-existing string literals byte-for-byte
(``StrEnum`` is a ``str`` subclass), so this is a purely additive,
wire-compatible change: existing string comparisons/consumers keep
working unchanged.
"""
BOOT = "pipeline.boot"
INTENT = "pipeline.intent"
ESCALATION = "pipeline.escalation"
SWARM_START = "pipeline.swarm.start"
SWARM_SHARD = "pipeline.swarm.shard"
SWARM_DONE = "pipeline.swarm.done"
REVIEW = "pipeline.review"
TEST_RESULT = "pipeline.test.result"
COMPLETE = "pipeline.complete"
STAGE = "pipeline.stage"
PAUSED = "pipeline.paused"
RESUMED = "pipeline.resumed"
CANCELLED = "pipeline.cancelled"
_logger = logging.getLogger(__name__)
# Redis key for event stream
EVENTS_KEY = "orchestrata:pipeline:events"
MAX_EVENTS = 500
# Per-thread Redis stream prefix the gateway's RedisRingBuffer reads from
# (it prefixes stream names with ``gw:ringbuf:``), so the UI topic
# ``pipeline.<thread_id>`` is durably backed and replayable on reconnect.
RINGBUF_PREFIX = "gw:ringbuf:"
PIPELINE_TOPIC_PREFIX = "pipeline."
# Global stream the gateway forwarder tails to fan out live events to all
# ``pipeline.<thread_id>`` subscribers across processes.
PIPELINE_STREAM_KEY = f"{RINGBUF_PREFIX}pipeline.events"
PIPELINE_STREAM_MAXLEN = 1000
def pipeline_topic(thread_id: str) -> str:
"""Return the UI pub/sub topic for a pipeline thread."""
return f"{PIPELINE_TOPIC_PREFIX}{thread_id}"
@traces(spec="001", story="US-006", reqs=["REQ-001-US-006-020"])
@dataclass
class PipelineEvent:
"""A milestone event in the pipeline lifecycle.
Attributes:
event_type: The event's type (``PipelineEventType`` member — a
``str`` subclass, so it serializes/compares identically to the
legacy dotted string literal).
thread_id: CXP thread this event belongs to.
volk: Volk alias that triggered this event.
data: Event-specific payload.
timestamp: When the event occurred.
iteration: Iteration number if known.
driver: Provenance of the execution that produced this event
(e.g. ``"inprocess"``, or ``"headless:<harness>"`` at Phase 2 —
VolkDriver Phase 0, SPEC-001 US-006). Empty string means
unattributed — legal, never an error.
"""
event_type: PipelineEventType
thread_id: str = ""
volk: str = ""
data: dict[str, Any] = field(default_factory=dict)
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
iteration: int = 0
driver: str = ""
def to_payload(self) -> dict[str, Any]:
"""The canonical broadcast serialization for this event.
Implemented as exactly ``asdict(self)`` so the REQ-006-032
golden-bytes wire-compat guard also pins this method: both
producers (the kernel bus and the fallback ``PipelineKernelClient``)
share this single serializer, so their payload shapes cannot drift.
"""
return asdict(self)
@traces(spec="001", story="US-006", reqs=["REQ-006-031", "REQ-006-033"])
def build_intent_event(thread_id: str, intent: str, volk: str = "") -> PipelineEvent:
"""Build a ``PipelineEventType.INTENT`` event — the single-source data
shape shared by the kernel bus (``emit_intent``) and the fallback
producer's legacy ``pipeline.start`` fold (``volk="U:Cx"``)."""
return PipelineEvent(
event_type=PipelineEventType.INTENT,
thread_id=thread_id,
volk=volk,
data={"intent": intent[:200]},
)
@traces(spec="001", story="US-006", reqs=["REQ-006-031", "REQ-006-033"])
def build_stage_event(
thread_id: str,
stage: str,
status: str,
duration_ms: float,
artifact_count: int,
error: str | None = None,
) -> PipelineEvent:
"""Build a ``PipelineEventType.STAGE`` event — one pipeline stage's
result. ``volk`` is the stage alias itself (the stage IS the actor)."""
return PipelineEvent(
event_type=PipelineEventType.STAGE,
thread_id=thread_id,
volk=stage,
data={
"stage": stage,
"status": status,
"duration_ms": duration_ms,
"artifact_count": artifact_count,
"error": error,
},
)
@traces(spec="001", story="US-006", reqs=["REQ-006-031", "REQ-006-033"])
def build_complete_event(
thread_id: str,
success: bool,
reason: str = "",
extra: dict[str, Any] | None = None,
) -> PipelineEvent:
"""Build a ``PipelineEventType.COMPLETE`` event. ``extra`` merges
additional data fields (e.g. ``stages_completed``/``total_artifacts``
from the legacy ``pipeline.complete`` fold, or ``error`` from the
legacy ``pipeline.error`` fold) on top of the canonical
``{success, reason}`` pair."""
data: dict[str, Any] = {"success": success, "reason": reason}
if extra:
data.update(extra)
return PipelineEvent(
event_type=PipelineEventType.COMPLETE,
thread_id=thread_id,
data=data,
)
@traces(spec="001", story="US-006", reqs=["REQ-006-031", "REQ-006-033"])
def build_paused_event(thread_id: str) -> PipelineEvent:
"""Build a ``PipelineEventType.PAUSED`` event."""
return PipelineEvent(event_type=PipelineEventType.PAUSED, thread_id=thread_id)
@traces(spec="001", story="US-006", reqs=["REQ-006-031", "REQ-006-033"])
def build_resumed_event(thread_id: str) -> PipelineEvent:
"""Build a ``PipelineEventType.RESUMED`` event."""
return PipelineEvent(event_type=PipelineEventType.RESUMED, thread_id=thread_id)
@traces(spec="001", story="US-006", reqs=["REQ-006-031", "REQ-006-033"])
def build_cancelled_event(thread_id: str) -> PipelineEvent:
"""Build a ``PipelineEventType.CANCELLED`` event."""
return PipelineEvent(event_type=PipelineEventType.CANCELLED, thread_id=thread_id)
class PipelineEventBus:
"""Publishes pipeline events to Redis and optional callbacks.
Attributes:
redis: Redis client (optional — events still logged if None).
callbacks: List of synchronous callback functions.
"""
def __init__(
self,
redis_client: Any | None = None,
events_key: str = EVENTS_KEY,
ui_stream_key: str | None = None,
driver: str = "inprocess",
) -> None:
"""Initialize the event bus.
Args:
redis_client: Redis client instance. If None, events
are only logged and sent to callbacks.
events_key: Redis list the bus publishes to. Production uses
the module default; tests pass a test-scoped key so
parallel suite batches can't interleave on one list.
ui_stream_key: Global Redis stream ``_forward_to_ui`` XADDs to
(mirrors the gateway pump's ``stream_key`` injection seam,
:func:`_pump_pipeline_events_once`). Defaults to
``PIPELINE_STREAM_KEY`` — zero behaviour change when unset.
driver: This bus's own producer identity (VolkDriver Phase 0,
SPEC-001 US-006). ``emit`` resolves it onto the OUTPUT
payload for any event the caller left unattributed —
``"inprocess"`` by default, ``"headless:<harness>"`` for a
Phase 2 headless-harness bus.
"""
self._redis: Any | None = redis_client
self._events_key = events_key
self._ui_stream_key = ui_stream_key or PIPELINE_STREAM_KEY
self._callbacks: list[Callable[[PipelineEvent], None]] = []
self._driver = driver
def on(self, callback: Callable[[PipelineEvent], None]) -> None:
"""Register a callback for all events.
Args:
callback: Function taking a PipelineEvent.
"""
self._callbacks.append(callback)
@traces(spec="001", story="US-006", reqs=["REQ-001-US-006-020"])
def emit(self, event: PipelineEvent) -> None:
"""Publish an event to Redis and all callbacks.
Resolves ``driver`` to this bus's own producer identity
(``self._driver``, "inprocess" by default) on the OUTPUT payload
when the caller left it unset — attribution belongs to the
SERIALIZED wire representation, never mutated onto the caller's
object (VolkDriver Phase 0). A caller-supplied driver (Phase 2
headless-harness producers) is never overridden. Callbacks receive
the original, unmutated *event* — driver resolution is a
wire/serialization concern, not an in-process one.
Args:
event: The pipeline event to publish.
"""
event_dict = event.to_payload()
event_dict["driver"] = event_dict["driver"] or self._driver
event_json = json.dumps(event_dict, default=str)
_logger.debug(
"[EVENT] %s volk=%s %s",
event.event_type,
event.volk,
json.dumps(event.data, default=str)[:100],
)
# Publish to Redis
if self._redis:
try:
self._redis.lpush(self._events_key, event_json)
self._redis.ltrim(self._events_key, 0, MAX_EVENTS - 1)
except Exception as exc:
_logger.warning("Event publish failed: %s", exc)
self._forward_to_ui(event, event_dict)
# Fire callbacks
for cb in self._callbacks:
try:
cb(event)
except Exception as exc:
_logger.warning("Event callback failed: %s", exc)
@traces(spec="001", story="US-006", reqs=["REQ-001-US-006-020"])
def _forward_to_ui(self, event: PipelineEvent, event_dict: dict[str, Any]) -> None:
"""Bridge an event to the gateway's UI plumbing (Redis streams).
Writes the event to two Redis streams the gateway already understands:
- ``gw:ringbuf:pipeline.<thread_id>`` — durable per-thread replay, read
directly by the gateway's ``RedisRingBuffer`` (which prefixes stream
names with ``gw:ringbuf:``), so reconnecting clients see prior events.
- ``gw:ringbuf:pipeline.events`` — a global stream the gateway forwarder
tails to broadcast live onto ``pipeline.<thread_id>``.
Cross-process safe: the kernel and gateway share Redis even when the
kernel runs out-of-process behind gRPC. Best-effort and non-fatal.
Args:
event: The event being emitted.
event_dict: ``asdict(event)`` (already computed by ``emit``).
"""
if not self._redis:
return
# Redis streams require flat string fields; nested data is JSON-encoded.
fields = {
"event_type": event.event_type,
"thread_id": event.thread_id,
"volk": event.volk,
"iteration": str(event.iteration),
"timestamp": event.timestamp,
"data": json.dumps(event.data, default=str),
"driver": event_dict["driver"],
}
try:
if event.thread_id:
self._redis.xadd(
f"{RINGBUF_PREFIX}{pipeline_topic(event.thread_id)}",
fields,
maxlen=PIPELINE_STREAM_MAXLEN,
approximate=True,
)
self._redis.xadd(
self._ui_stream_key,
fields,
maxlen=PIPELINE_STREAM_MAXLEN,
approximate=True,
)
except Exception as exc:
_logger.warning("UI event forward failed: %s", exc)
@traces(spec="001", story="US-008", reqs=["REQ-001-US-008-007"])
def emit_boot(self, volks: list[str], thread_id: str = "") -> None:
"""Emit pipeline.boot event.
Args:
volks: List of registered volk aliases.
thread_id: Thread ID if available.
"""
self.emit(
PipelineEvent(
event_type=PipelineEventType.BOOT,
thread_id=thread_id,
data={"volks": volks, "count": len(volks)},
)
)
def emit_intent(
self,
thread_id: str,
intent: str,
) -> None:
"""Emit pipeline.intent event.
Args:
thread_id: CXP thread ID.
intent: The submitted intent text.
"""
self.emit(build_intent_event(thread_id, intent))
@traces(spec="001", story="US-008", reqs=["REQ-001-US-008-003"])
def emit_escalation(
self,
thread_id: str,
from_volk: str,
to_volk: str,
subject: str,
) -> None:
"""Emit pipeline.escalation event.
Args:
thread_id: CXP thread ID.
from_volk: Source volk alias.
to_volk: Destination volk alias.
subject: CXP packet subject.
"""
self.emit(
PipelineEvent(
event_type=PipelineEventType.ESCALATION,
thread_id=thread_id,
volk=from_volk,
data={
"from": from_volk,
"to": to_volk,
"subject": subject,
},
)
)
def emit_swarm_start(
self,
thread_id: str,
shard_count: int,
) -> None:
"""Emit pipeline.swarm.start event.
Args:
thread_id: CXP thread ID.
shard_count: Number of parallel shards.
"""
self.emit(
PipelineEvent(
event_type=PipelineEventType.SWARM_START,
thread_id=thread_id,
volk="U:Cd",
data={"shard_count": shard_count},
)
)
def emit_swarm_shard(
self,
thread_id: str,
shard_id: int,
file_path: str,
success: bool,
) -> None:
"""Emit pipeline.swarm.shard event.
Args:
thread_id: CXP thread ID.
shard_id: Shard number.
file_path: File this shard generated.
success: Whether the shard succeeded.
"""
self.emit(
PipelineEvent(
event_type=PipelineEventType.SWARM_SHARD,
thread_id=thread_id,
volk="J:Junior",
data={
"shard_id": shard_id,
"file": file_path,
"success": success,
},
)
)
def emit_swarm_done(
self,
thread_id: str,
succeeded: int,
total: int,
) -> None:
"""Emit pipeline.swarm.done event.
Args:
thread_id: CXP thread ID.
succeeded: Number of successful shards.
total: Total shards.
"""
self.emit(
PipelineEvent(
event_type=PipelineEventType.SWARM_DONE,
thread_id=thread_id,
volk="U:Cd",
data={"succeeded": succeeded, "total": total},
)
)
def emit_review(
self,
thread_id: str,
volk: str,
review_type: str,
passed: bool,
cycle: int = 0,
issues: int = 0,
) -> None:
"""Emit pipeline.review event.
Args:
thread_id: CXP thread ID.
volk: Reviewing volk alias.
review_type: "self_review" or "deterministic".
passed: Whether the review passed.
cycle: Review cycle number.
issues: Number of issues found.
"""
self.emit(
PipelineEvent(
event_type=PipelineEventType.REVIEW,
thread_id=thread_id,
volk=volk,
data={
"type": review_type,
"passed": passed,
"cycle": cycle,
"issues": issues,
},
)
)
def emit_test_result(
self,
thread_id: str,
passed: bool,
pass_count: int,
fail_count: int,
coverage_pct: float = 0.0,
duration_s: float = 0.0,
) -> None:
"""Emit pipeline.test.result event.
Args:
thread_id: CXP thread ID.
passed: Whether all tests passed.
pass_count: Number of passed tests.
fail_count: Number of failed tests.
coverage_pct: Line coverage percentage.
duration_s: Test execution time.
"""
self.emit(
PipelineEvent(
event_type=PipelineEventType.TEST_RESULT,
thread_id=thread_id,
volk="U:Qa",
data={
"passed": passed,
"pass_count": pass_count,
"fail_count": fail_count,
"coverage_pct": coverage_pct,
"duration_s": duration_s,
},
)
)
def emit_complete(
self,
thread_id: str,
success: bool,
reason: str = "",
) -> None:
"""Emit pipeline.complete event.
Args:
thread_id: CXP thread ID.
success: Whether the pipeline succeeded.
reason: Completion reason (e.g. "tests_pass", "shutdown").
"""
self.emit(build_complete_event(thread_id, success, reason))
def emit_stage(
self,
thread_id: str,
stage: str,
status: str,
duration_ms: float,
artifact_count: int,
error: str | None = None,
) -> None:
"""Emit pipeline.stage event — a pipeline stage's result.
Args:
thread_id: CXP thread ID.
stage: Stage alias (also used as ``volk`` — the stage IS the actor).
status: Stage outcome (e.g. "resolved", "failed").
duration_ms: How long the stage took, in milliseconds.
artifact_count: Number of artifacts the stage produced.
error: Error message if the stage failed, else None.
"""
self.emit(
build_stage_event(
thread_id, stage, status, duration_ms, artifact_count, error
)
)
def emit_paused(self, thread_id: str) -> None:
"""Emit pipeline.paused event.
Args:
thread_id: CXP thread ID.
"""
self.emit(build_paused_event(thread_id))
def emit_resumed(self, thread_id: str) -> None:
"""Emit pipeline.resumed event.
Args:
thread_id: CXP thread ID.
"""
self.emit(build_resumed_event(thread_id))
def emit_cancelled(self, thread_id: str) -> None:
"""Emit pipeline.cancelled event.
Args:
thread_id: CXP thread ID.
"""
self.emit(build_cancelled_event(thread_id))