Symfony Event Delivery Model
packages/symfony owns the framework-facing delivery model for InstructorPHP runtime events.
The core rule is:
- the package-owned
Cognesy\Events\Contracts\CanHandleEventsservice is the authoritative runtime event bus - Symfony’s
event_dispatcheris an optional observation bridge, not the source of truth - transport-specific delivery such as Messenger handoff or HTTP streaming layers on top of those semantics instead of redefining them
Delivery Surfaces
The package supports four distinct delivery surfaces:- Internal runtime bus This carries the full event stream and remains the place where wiretaps, telemetry, and logging attach.
- Projected progress bus
This carries
RuntimeProgressUpdateobjects derived from the internal runtime stream. Web, API, and CLI code can build on this stable projection without consuming every raw runtime event directly. - Symfony EventDispatcher bridge This mirrors the supported observation subset into Symfony so applications can use listeners, subscribers, and queued listener patterns.
- Async delivery seams These are package-owned Messenger integration points for queued execution and observation workflows.
Event Categories
The runtime event stream should be reasoned about in three categories:1. Lifecycle events
These describe meaningful state transitions and are safe to bridge into Symfony by default. Examples:- extraction started, completed, failed
- response validated, transformed, or failed
- HTTP client built
- AgentCtrl execution started, completed, failed
- native-agent execution started, stepped, completed, failed
2. Observation detail events
These are still first-class runtime events, but they are primarily for low-level logging, telemetry enrichment, or debugging. Examples:- partial-response generation
- streamed tool-call updates
- raw chunk-received notifications
- verbose transport diagnostics
3. Internal infrastructure events
These exist to support package internals and should not define the public application-facing observation contract. Examples:- framework-specific adapter bootstrap details
- internal projector lifecycle hooks
- future delivery helper internals
Bridging Rules
The current and intended bridge semantics are:instructor.events.dispatch_to_symfony: trueenables the framework bridgeinstructor.events.dispatch_to_symfony: falsekeeps all observation package-local- the package-owned runtime bus still dispatches the full event stream either way
- framework listeners must never be required for core runtime correctness
- bridge lifecycle events by default
- keep high-frequency streaming or debug events on the internal wiretap path unless applications opt in later
- treat logging and telemetry as consumers of the internal bus, not as a side effect of Symfony listener registration
Runtime Shape Expectations
HTTP and API applications
- use the package-owned bus for correctness, logging, and telemetry
- use
Cognesy\Instructor\Symfony\Delivery\Progress\Contracts\CanHandleProgressUpdateswhen you want stable lifecycle or streaming-friendly projection - use the Symfony bridge for app-local listeners, notifications, and integration code
- build SSE, WebSocket, Mercure, or polling responses on top of
RuntimeProgressUpdaterather than assuming every raw runtime event becomes a framework event
Messenger workers
- Messenger is the primary package-supported async execution model
- queued handlers should consume explicit package-owned messages or handoff references, not raw framework event forwarding alone
- the same runtime event semantics must remain valid inside workers even when there is no active HTTP request
ExecuteAgentCtrlPromptMessagequeues AgentCtrl prompt execution against themessengerruntime adapterExecuteNativeAgentPromptMessagequeues native agent prompt execution against the package-owned session runtime and can resume a persisted session whensessionIdis providedRuntimeObservationMessagecarries explicitly selected runtime events into Messenger for queued observation workflows
- execution uses package-owned message DTOs and handlers
- observation uses an allow-listed wiretap bridge from the internal event bus into the configured Symfony bus
- raw Symfony listener mirroring is still distinct from Messenger queue dispatch
CLI applications
- CLI flows should keep using the same internal bus and wiretap semantics
- framework event bridging is optional, not required
- the package now exposes
SymfonyCliObservationFormatterandSymfonyCliObservationPrinteron top of the projected progress bus instructor.delivery.cli.enabled: trueauto-attaches the built-in printer to that progress bus- CLI observation helpers still format the projected runtime stream instead of inventing a second event model
Progress Projection
The package now owns a dedicated progress projection seam:- raw runtime events stay on
Cognesy\Events\Contracts\CanHandleEvents - projected progress updates flow through
Cognesy\Instructor\Symfony\Delivery\Progress\Contracts\CanHandleProgressUpdates - consumers receive
Cognesy\Instructor\Symfony\Delivery\Progress\RuntimeProgressUpdate
startedprogressstreamcompletedfailed
- native-agent lifecycle and step events
- AgentCtrl execution and streaming events
- structured-output lifecycle and partial-response events
- low-level HTTP streaming diagnostics
Relation To Logging And Telemetry
Logging and telemetry need access to the broadest and most coherent event stream. Because of that:- package-owned logging attaches to the internal event bus
- telemetry projectors and exporters should also prefer the internal event stream
- Symfony EventDispatcher mirroring is a convenience integration surface for applications, not the authoritative observability pipeline
Public Contract For Later Tasks
Later tasks should preserve these rules:packages/symfonyowns framework registration and delivery defaultspackages/eventskeeps reusable bridge primitives such asSymfonyEventDispatcher- Messenger integration should move execution or observation work explicitly, not by abusing framework event mirroring as a transport
- transport-specific streaming remains optional and layered under
instructor.delivery - the internal bus remains the only place guaranteed to see the full event stream