emitToolsProgressively loads tools from each toolset and emits progress updates. This allows the UI to show the tool count incrementally as each toolset loads, with a spinner indicating that more tools may be coming.
(ctx context.Context, a *agent.Agent, send func(Event) bool)
| 1558 | // This allows the UI to show the tool count incrementally as each toolset loads, |
| 1559 | // with a spinner indicating that more tools may be coming. |
| 1560 | func (r *LocalRuntime) emitToolsProgressively(ctx context.Context, a *agent.Agent, send func(Event) bool) { |
| 1561 | toolsets := a.ToolSets() |
| 1562 | totalToolsets := len(toolsets) |
| 1563 | |
| 1564 | // If no toolsets, emit final state immediately |
| 1565 | if totalToolsets == 0 { |
| 1566 | send(ToolsetInfo(0, false, r.currentAgentName())) |
| 1567 | return |
| 1568 | } |
| 1569 | |
| 1570 | // Emit initial loading state |
| 1571 | if !send(ToolsetInfo(0, true, r.currentAgentName())) { |
| 1572 | return |
| 1573 | } |
| 1574 | |
| 1575 | // Load tools from each toolset and emit progress |
| 1576 | var totalTools int |
| 1577 | for i, toolset := range toolsets { |
| 1578 | // Check context before potentially slow operations |
| 1579 | if ctx.Err() != nil { |
| 1580 | return |
| 1581 | } |
| 1582 | |
| 1583 | isLast := i == totalToolsets-1 |
| 1584 | |
| 1585 | // Start the toolset if needed, including recovery: a previously-started |
| 1586 | // toolset whose inner connection died (e.g. background invalid_token) |
| 1587 | // must have its recovery Start() called here so ShouldReportRecoveryFailure |
| 1588 | // can fire the targeted re-auth notice. Start() is a no-op when the |
| 1589 | // toolset is already healthy, so calling it unconditionally is safe. |
| 1590 | if startable, ok := toolset.(*tools.StartableToolSet); ok { |
| 1591 | if err := startToolsetWithTimeout(ctx, startable, r.toolStartTimeout); err != nil { |
| 1592 | desc := tools.DescribeToolSet(startable.ToolSet) |
| 1593 | // A start that outlived its deadline (e.g. an MCP container |
| 1594 | // stuck behind a wedged Docker daemon) is reported directly: |
| 1595 | // the abandoned Start goroutine has not returned, so the |
| 1596 | // once-per-streak guard below has recorded nothing and would |
| 1597 | // silently swallow the warning. |
| 1598 | if errors.Is(err, context.DeadlineExceeded) && ctx.Err() == nil { |
| 1599 | slog.WarnContext(ctx, "Toolset start timed out; skipping", |
| 1600 | "agent", a.Name(), "toolset", desc, "timeout", r.toolStartTimeout) |
| 1601 | a.AddToolWarning(fmt.Sprintf("%s is taking too long to start (>%s) — it keeps starting in the background and its tools appear once it is ready", desc, r.toolStartTimeout)) |
| 1602 | continue |
| 1603 | } |
| 1604 | // IsAuthorizationRequired must be checked BEFORE |
| 1605 | // ShouldReportFailure: this is the first — expected — |
| 1606 | // failure of a deferred-OAuth toolset, and consuming the |
| 1607 | // failure-reported flag here would suppress the *real* |
| 1608 | // failure (e.g. server 4xx on the eventual interactive |
| 1609 | // retry) that the user actually needs to see. |
| 1610 | if mcptools.IsAuthorizationRequired(err) { |
| 1611 | // Two cases: |
| 1612 | // 1. Initial startup deferral (toolset never ran): the |
| 1613 | // OAuth dialog will appear naturally on the first user |
| 1614 | // message — no need to pre-announce it. |
| 1615 | // 2. Recovery: the toolset was previously working but the |
| 1616 | // background watcher detected a server-side invalid_token |
| 1617 | // (fixes #3198). Surface a deduped re-auth notice so the |