finalizeEventChannel performs cleanup at the end of a RunStream goroutine: emits the StreamStopped event, fires hooks, records telemetry, restores the previous elicitation channel, and closes the events channel. reason is one of the turnEndReason* constants and classifies how the stream ended (e.g.
(ctx context.Context, sess *session.Session, reason string, prevElicitationCh, events chan Event)
| 195 | // Consumers must rely on the channel close, not on receiving StreamStopped, as |
| 196 | // the guaranteed terminal signal. |
| 197 | func (r *LocalRuntime) finalizeEventChannel(ctx context.Context, sess *session.Session, reason string, prevElicitationCh, events chan Event) { |
| 198 | a := r.resolveSessionAgent(sess) |
| 199 | |
| 200 | if ctx.Err() != nil && reason == "" { |
| 201 | reason = turnEndReasonCanceled |
| 202 | } |
| 203 | |
| 204 | // Best-effort, non-blocking on purpose: a blocking send here reintroduces |
| 205 | // the #3070 teardown deadlock. See the doc comment for the ordering and |
| 206 | // delivery contract. |
| 207 | nonBlocking(&channelSink{ch: events}).Emit(StreamStopped(sess.ID, a.Name(), reason)) |
| 208 | |
| 209 | // Execute session end hooks with a context that won't be cancelled so |
| 210 | // cleanup hooks run even when the stream was interrupted (e.g. Ctrl+C). |
| 211 | r.executeSessionEndHooks(context.WithoutCancel(ctx), sess, a) |
| 212 | |
| 213 | r.executeOnUserInputHooks(ctx, sess.ID, "stream stopped") |
| 214 | |
| 215 | r.telemetry.RecordSessionEnd(ctx) |
| 216 | |
| 217 | r.elicitation.restoreAndClose(events, prevElicitationCh) |
| 218 | } |
| 219 | |
| 220 | // RunStream starts the agent's interaction loop and returns a channel of events. |
| 221 | // The returned channel is closed when the loop terminates (success, error, or |