Skip to content

Events

Overview

Instructor dispatches events at every significant stage of its execution. You can listen to these events for logging, monitoring, debugging, or custom processing. All event classes extend Cognesy\Events\Event.

Listening to Events

Targeted Listeners

Use onEvent() on the StructuredOutputRuntime to listen for a specific event type:

use Cognesy\Instructor\Events\Response\ResponseValidationFailed;

$runtime = StructuredOutputRuntime::fromDefaults()
    ->onEvent(ResponseValidationFailed::class, function ($event) {
        logger()->warning('Validation failed', $event->toArray());
    });
// @doctest id="0608"

Wiretap (All Events)

Use wiretap() to receive every event dispatched by Instructor. This is useful for debugging or comprehensive logging:

$runtime = StructuredOutputRuntime::fromDefaults()
    ->wiretap(fn($event) => $event->print());
// @doctest id="3ecf"

Practical Example

use Cognesy\Instructor\StructuredOutput;
use Cognesy\Instructor\StructuredOutputRuntime;
use Cognesy\Instructor\Extras\Scalar\Scalar;
use Cognesy\Http\Events\HttpRequestSent;
use Cognesy\Http\Events\HttpResponseReceived;

$runtime = StructuredOutputRuntime::fromDefaults()
    // Log HTTP-level details
    ->onEvent(HttpRequestSent::class, fn($e) => dump($e))
    ->onEvent(HttpResponseReceived::class, fn($e) => dump($e))
    // Console-friendly output for all events
    ->wiretap(fn($event) => $event->print())
    // Structured logging
    ->wiretap(fn($event) => YourLogger::log($event->asLog()));

$result = (new StructuredOutput($runtime))
    ->with(
        messages: 'What is the population of Paris?',
        responseModel: Scalar::integer(),
    )
    ->get();
// @doctest id="800e"

Event Categories

Events are organized into namespaces that correspond to the processing stage:

High-Level Events (Events\StructuredOutput)

Event When
StructuredOutputStarted A structured output operation begins
StructuredOutputRequestReceived The request has been received by the runtime
StructuredOutputResponseGenerated The final StructuredOutputResponse has been produced
StructuredOutputResponseUpdated A streaming partial response is emitted

Response-model Events (Events\ResponseModel)

Event When
ResponseModelRequested A response model has been submitted for processing
ResponseModelBuildModeSelected The factory has chosen a build strategy
ResponseModelBuilt The response model and schema are ready

Attempt Events (Events\Attempt)

Event When
ResponseRetryScheduled A retry attempt has been scheduled
ResponseRecoveryExhausted All retries have been exhausted

Response Events (Events\Response)

Event When
ResponseDeserializationAttempt Deserialization is about to start
ResponseDeserialized Deserialization succeeded
ResponseDeserializationFailed Deserialization failed
CustomResponseDeserializationAttempt A CanDeserializeSelf implementation is being used
ResponseValidationAttempt Validation is about to start
ResponseValidated Validation passed
ResponseValidationFailed Validation failed
CustomResponseValidationAttempt A CanValidateSelf implementation is being used
ResponseTransformationAttempt Transformation is about to start
ResponseTransformed Transformation succeeded
ResponseTransformationFailed Transformation failed
ResponseMaterialized Final materialization succeeded, with the actual result type
ResponseMaterializationFailed Final materialization failed at a typed stage

Extraction Events (Events\Extraction)

Event When
ExtractionStarted Data extraction from the inference response begins
ExtractionCompleted Extraction succeeded
ExtractionFailed Extraction failed
ExtractionStrategyAttempted A specific extraction strategy is being tried
ExtractionStrategySucceeded The strategy produced a result
ExtractionStrategyFailed The strategy did not produce a result

Streaming Events (Events\Streaming)

Event When
ChunkReceived A raw chunk arrived from the provider
StreamedResponseReceived A streamed response chunk was processed
StreamedResponseFinished The stream has ended
PartialJsonReceived A partial JSON fragment was accumulated
PartialResponseGenerated A partial deserialized response is available
PartialResponseGenerationFailed Partial deserialization failed (non-fatal)
StreamedToolCallStarted A tool call began in the stream
StreamedToolCallUpdated A tool call received more data
StreamedToolCallCompleted A tool call finished
SequenceUpdated A sequence item has been completed

Listener Gating

Some events are not constructed at all when nothing is listening for them. Building the payload is not free: the structured-output lifecycle events each carry a telemetry envelope that serialises the entire conversation, and StructuredOutputResponseGenerated additionally normalizes the result value, runs strlen() over the content and reasoning content, and walks the tool calls. StructuredOutputResponseUpdated pays a smaller version of that once per streamed emission.

The rule is Cognesy\Events\Support\ListenerGate, shared with Polyglot — see the "Listener Gating" section of packages/polyglot/docs/internals/events.md for the two properties that matter to anyone writing a dispatcher.

What is gated

Emitter Events
StructuredOutputEventProjector StructuredOutputRequestReceived, StructuredOutputStarted, StructuredOutputResponseUpdated, StructuredOutputResponseGenerated
DispatchStreamingEventsReducer ChunkReceived, PartialResponseGenerated, SequenceUpdated, StreamedResponseReceived, the StreamedToolCall* family

Everything else is dispatched unconditionally.

Fail-open is contractual. A dispatcher that does not implement Cognesy\Events\Contracts\CanCheckListeners cannot report its listeners, so it is assumed to listen and receives every event. No dispatcher ever loses an event to this optimisation.

When the gate is resolved

StructuredOutputEventProjector resolves its gates once, at construction, and a projector is built per execution — by StructuredOutputExecutionSession for the sync path and inside StructuredOutputStream::__construct() for the streaming path. Both are constructed after any onEvent() or wiretap() call on the runtime, so no listener can be registered and then missed.

StructuredOutputRuntime is the exception: it builds its projector per request, inside create(). A runtime is long-lived and onEvent() mutates it, so gates resolved in its constructor would silently drop StructuredOutputRequestReceived for every caller who registers a listener the way the API invites. One hasListenersFor() per request costs nothing next to the envelope it guards.

DefaultRetryPolicy also builds a projector, but the retry and recovery events it emits are dispatched unconditionally — only the four events in the table above are gated.

Only the payload and the dispatch are conditional. Timing, attempt numbering and execution state are not.

Event Methods

Every event inherits the following convenience methods from Cognesy\Events\Event:

Method Description
print() Print a console-friendly representation
printLog() Print a log-formatted representation
printDebug() Print console output and dump the full event object
asConsole() Return the event formatted for console output
asLog() Return the event formatted for log output
toArray() Return the event data as an associative array
name() Return the short class name of the event

Events carry a $logLevel property (PSR log level) and a $data payload. The print() method respects a configurable log-level threshold.

Instructor event payloads are normalized arrays. Structured-output lifecycle events also expose correlation fields such as requestId, executionId, attemptId, phase, and phaseId where applicable.