* Run an agent
({
agent,
forwardedProps,
resume,
}: CopilotKitCoreRunAgentParams)
| 300 | * Run an agent |
| 301 | */ |
| 302 | async runAgent({ |
| 303 | agent, |
| 304 | forwardedProps, |
| 305 | resume, |
| 306 | }: CopilotKitCoreRunAgentParams): Promise<RunAgentResult> { |
| 307 | // Agent ID is guaranteed to be set by validateAndAssignAgentId |
| 308 | if (agent.agentId) { |
| 309 | void this._internal.suggestionEngine.clearSuggestions(agent.agentId); |
| 310 | } |
| 311 | |
| 312 | // Re-apply core headers (merged on top of the agent's own headers) so a |
| 313 | // late header update is picked up without clobbering per-agent headers. |
| 314 | this._internal.applyHeadersToAgent(agent); |
| 315 | |
| 316 | // Detach any active run (e.g. a long-lived connectAgent pipeline) before |
| 317 | // starting a new run. We await the detach to ensure the previous pipeline |
| 318 | // has fully finalized — its activeRunCompletionPromise resolves once the |
| 319 | // observable finalize block runs, which happens synchronously after the |
| 320 | // takeUntil signal completes the chain. This prevents a race where the new |
| 321 | // runAgent() overwrites activeRunDetach$ / activeRunCompletionPromise before |
| 322 | // the old pipeline can clean up, causing dropped runs. |
| 323 | // |
| 324 | // Historical note: an earlier version used fire-and-forget (`void`) here |
| 325 | // because awaiting caused a deadlock when connectAgent's |
| 326 | // ConnectNotImplementedError cleanup was still in-flight. That deadlock |
| 327 | // was resolved in @ag-ui/client ≥0.0.42 where the catchError path |
| 328 | // (ConnectNotImplementedError → EMPTY) always runs the finalize block, |
| 329 | // so the completion promise now resolves reliably. |
| 330 | if (agent.detachActiveRun) { |
| 331 | await agent.detachActiveRun(); |
| 332 | } |
| 333 | |
| 334 | // Set up abort controller and agent.abortRun() intercept only for the |
| 335 | // top-level call. Recursive follow-up calls from processAgentResult |
| 336 | // reuse the same controller. |
| 337 | const isTopLevel = this._runDepth === 0; |
| 338 | let originalAbortRun: (() => void) | undefined; |
| 339 | |
| 340 | if (isTopLevel) { |
| 341 | this._runAbortController = new AbortController(); |
| 342 | |
| 343 | // Intercept agent.abortRun() so that calling it directly (not via |
| 344 | // stopAgent) also aborts in-flight tool execution and prevents |
| 345 | // follow-up runs. |
| 346 | const controller = this._runAbortController; |
| 347 | originalAbortRun = agent.abortRun.bind(agent); |
| 348 | agent.abortRun = () => { |
| 349 | controller.abort(); |
| 350 | originalAbortRun!(); |
| 351 | }; |
| 352 | |
| 353 | // Notify subscribers (e.g. the inspector) about the agent that is about |
| 354 | // to run. Per-thread clones are not in the agent registry, so |
| 355 | // onAgentsChanged never fires for them and they would otherwise be |
| 356 | // invisible to subscribers. Fired once per top-level run; recursive |
| 357 | // follow-up runs reuse the same instance and need no re-notification. |
| 358 | await this._internal.notifySubscribers( |
| 359 | (subscriber) => |
no test coverage detected