runTurn performs one iteration of the run-stream loop, from turn_start onwards. Wrapping the body in its own function exists for one reason: a deferred call can fire turn_end on every exit path — a normal stop, an error from handleStreamError, a hook-driven shutdown, the loop detector, context cance
( ctx context.Context, sess *session.Session, a *agent.Agent, m *modelsdev.Model, model provider.Provider, modelID modelsdev.ID, contextLimit int64, sessionSpan trace.Span, agentTools []tools.Tool, ls *loopState, events EventSink, )
| 625 | // (overflowCompactions, toolModelOverride) is mutated through the |
| 626 | // shared loopState pointer. |
| 627 | func (r *LocalRuntime) runTurn( |
| 628 | ctx context.Context, |
| 629 | sess *session.Session, |
| 630 | a *agent.Agent, |
| 631 | m *modelsdev.Model, |
| 632 | model provider.Provider, |
| 633 | modelID modelsdev.ID, |
| 634 | contextLimit int64, |
| 635 | sessionSpan trace.Span, |
| 636 | agentTools []tools.Tool, |
| 637 | ls *loopState, |
| 638 | events EventSink, |
| 639 | ) turnControl { |
| 640 | streamAttrs := []attribute.KeyValue{ |
| 641 | attribute.String(genai.AttrConversationID, sess.ID), |
| 642 | attribute.String(genai.AttrAgentNameRuntime, a.Name()), |
| 643 | } |
| 644 | if genai.EmitLegacyAttributes() { |
| 645 | streamAttrs = append(streamAttrs, |
| 646 | attribute.String("agent", a.Name()), |
| 647 | attribute.String("session.id", sess.ID), |
| 648 | ) |
| 649 | } |
| 650 | streamCtx, streamSpan := r.startSpan(ctx, "runtime.stream", trace.WithAttributes(streamAttrs...)) |
| 651 | // streamSpan ends inline at the natural points (success path before |
| 652 | // recordAssistantMessage, error path after handleStreamError) so its |
| 653 | // duration tracks the model call only, not the whole iteration. The |
| 654 | // boolean prevents a double-End on paths that already closed it. |
| 655 | spanEnded := false |
| 656 | endStreamSpan := func() { |
| 657 | if !spanEnded { |
| 658 | streamSpan.End() |
| 659 | spanEnded = true |
| 660 | } |
| 661 | } |
| 662 | defer endStreamSpan() |
| 663 | |
| 664 | // endReason is set by every exit branch below and read by the |
| 665 | // deferred turn_end dispatch. Default = normal so a clean fall- |
| 666 | // through (model produced output, more tool calls, no hook |
| 667 | // blocked) reports "continue" or "normal" depending on which |
| 668 | // branch ran last. Branches overwrite this before returning. |
| 669 | endReason := turnEndReasonNormal |
| 670 | defer func() { |
| 671 | if ctxErr := ctx.Err(); ctxErr != nil && endReason == turnEndReasonNormal { |
| 672 | // Context cancellation is detected after the fact: a |
| 673 | // branch that returned early because of ctx.Err overrides |
| 674 | // the default, but a panic-recovered branch may not have |
| 675 | // had the chance, so re-check here. |
| 676 | endReason = turnEndReasonCanceled |
| 677 | } |
| 678 | // Use a non-cancellable context so turn_end runs even when |
| 679 | // the stream was interrupted (Ctrl+C, parent cancellation), |
| 680 | // matching the same guarantee session_end has at the |
| 681 | // finalizeEventChannel level. |
| 682 | r.executeTurnEndHooks(context.WithoutCancel(ctx), sess, a, endReason, events) |
| 683 | ls.exitReason = endReason |
| 684 | }() |
no test coverage detected