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
These are the events that application listeners, business monitoring, and queued follow-up work should primarily consume.
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
These should remain available on the package-owned bus and through wiretaps even when they are not mirrored into Symfony's dispatcher by default.
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
These can stay internal to the package-owned bus unless a later task promotes them into the supported observation surface explicitly.
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
The bridge should favor stable lifecycle events over noisy streaming internals.
Recommended long-term rule set:
- 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
This keeps Symfony listeners useful without forcing every application to absorb the hottest parts of the runtime stream.
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
Current package-owned Messenger seams:
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
Current config entrypoint:
instructor:
delivery:
messenger:
enabled: true
bus_service: message_bus
observe_events:
- Cognesy\Agents\Session\Events\SessionSaved
This is intentionally opt-in and explicit:
- 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
That projection currently classifies updates into:
startedprogressstreamcompletedfailed
Supported sources currently include:
- native-agent lifecycle and step events
- AgentCtrl execution and streaming events
- structured-output lifecycle and partial-response events
- low-level HTTP streaming diagnostics
This gives applications one stable hook for transport-specific delivery code without forcing the full raw event surface onto every consumer.
Example:
use Cognesy\Instructor\Symfony\Delivery\Progress\Contracts\CanHandleProgressUpdates;
use Cognesy\Instructor\Symfony\Delivery\Progress\RuntimeProgressUpdate;
$progress = $container->get(CanHandleProgressUpdates::class);
$progress->wiretap(static function (object $event): void {
if (! $event instanceof RuntimeProgressUpdate) {
return;
}
$payload = [
'status' => $event->status->value,
'source' => $event->source,
'message' => $event->message,
'operationId' => $event->operationId,
];
});
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
This prevents logging, telemetry, and Symfony listeners from drifting into separate interpretations of the same runtime behavior.
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
This is the semantic baseline for the later EventDispatcher, Messenger, telemetry, logging, and CLI observation tasks in the Symfony epic.