runForwarding runs a child session synchronously, forwarding all of its events to evts and propagating tool-approval state back to the parent on completion. This is the "interactive" path used by transfer_task and run_skill: the parent loop is blocked while the child executes, and the user sees the
(ctx context.Context, parent *session.Session, evts EventSink, req delegationRequest)
| 263 | // building the sub-session, driving RunStream, and recording the |
| 264 | // sub-session on the parent. |
| 265 | func (r *LocalRuntime) runForwarding(ctx context.Context, parent *session.Session, evts EventSink, req delegationRequest) (*tools.ToolCallResult, error) { |
| 266 | span := trace.SpanFromContext(ctx) |
| 267 | |
| 268 | callerAgent, err := r.team.Agent(r.currentAgentName()) |
| 269 | if err != nil { |
| 270 | return nil, fmt.Errorf("current agent not found: %w", err) |
| 271 | } |
| 272 | child, err := r.team.Agent(req.AgentName) |
| 273 | if err != nil { |
| 274 | return nil, err |
| 275 | } |
| 276 | |
| 277 | if req.SwitchCurrentAgent { |
| 278 | defer r.swapCurrentAgent(ctx, parent.ID, callerAgent, child, evts)() |
| 279 | } |
| 280 | |
| 281 | s := newSubSession(parent, req.SubSessionConfig, child) |
| 282 | |
| 283 | // subagent_stop fires after the child's stream has fully drained, |
| 284 | // using the *parent* agent's executor so handlers configured on the |
| 285 | // orchestrator see every child completion in one place — success or |
| 286 | // failure. The deferred call ensures we don't lose the event when an |
| 287 | // ErrorEvent triggers an early return below; handlers can detect a |
| 288 | // failed run by an empty stop_response (or by correlating with the |
| 289 | // session-level error event the parent already received). |
| 290 | defer func() { |
| 291 | r.executeSubagentStopHooks(ctx, parent, s, callerAgent, req.AgentName, s.GetLastAssistantMessageContent()) |
| 292 | }() |
| 293 | |
| 294 | childEvents := r.RunStream(ctx, s) |
| 295 | var subSessionErr error |
| 296 | for event := range childEvents { |
| 297 | evts.Emit(event) |
| 298 | if errEvent, ok := event.(*ErrorEvent); ok && subSessionErr == nil { |
| 299 | // Capture the first ErrorEvent but keep draining the channel so |
| 300 | // the sub-session's full transcript still streams through. The |
| 301 | // child's run loop may emit additional events (e.g. notifications, |
| 302 | // hook output) after the error before its channel closes; dropping |
| 303 | // them here would leave the TUI's streamDepth counter unbalanced |
| 304 | // and the user without context for what actually went wrong. |
| 305 | subSessionErr = fmt.Errorf("%s", errEvent.Error) |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | // Persist the sub-session unconditionally — even on error, the partial |
| 310 | // transcript is the most valuable artifact for debugging. The persistence |
| 311 | // pipeline relies on SubSessionCompleted to write the sub-session's |
| 312 | // messages to the store; without this emission they are silently dropped. |
| 313 | parent.AddSubSession(s) |
| 314 | evts.Emit(SubSessionCompleted(parent.ID, s, callerAgent.Name())) |
| 315 | |
| 316 | if subSessionErr != nil { |
| 317 | span.RecordError(subSessionErr) |
| 318 | span.SetStatus(codes.Error, "sub-session error") |
| 319 | return nil, subSessionErr |
| 320 | } |
| 321 | |
| 322 | // Only propagate ToolsApproved on success. A failed sub-session must not |
no test coverage detected