{"version":3,"file":"handle-suggest.mjs","names":[],"sources":["../../../../src/v2/runtime/handlers/handle-suggest.ts"],"sourcesContent":["import type { AbstractAgent, BaseEvent } from \"@ag-ui/client\";\nimport { finalizeRunEvents } from \"@copilotkit/shared\";\nimport { Observable } from \"rxjs\";\nimport type { CopilotRuntimeLike } from \"../core/runtime\";\nimport {\n  mergeForwardableHeaders,\n  resolveForwardHeadersPolicy,\n} from \"./header-utils\";\nimport { cloneAgentForRequest, parseRunRequest } from \"./shared/agent-utils\";\nimport { createSseEventResponse } from \"./shared/sse-response\";\n\n/**\n * Parameters for {@link handleSuggestAgent}.\n */\ninterface SuggestAgentParameters {\n  /** The runtime whose registry the agent is resolved from. */\n  runtime: CopilotRuntimeLike;\n  /** The incoming `POST /agent/:agentId/suggest` request. */\n  request: Request;\n  /** The route-resolved agent id to run. */\n  agentId: string;\n}\n\n/**\n * Stateless suggestion run.\n *\n * Executes the provider agent **directly** and streams its AG-UI events back as\n * SSE — the same wire format as `/agent/:id/run`, so the client consumes it with\n * the stock `HttpAgent` transport and chips fill in as the provider emits them.\n * It deliberately does not go through `runtime.runner`: the runner's\n * `run()` writes to a module-level store keyed by threadId (backing the SSE\n * runtime's local thread endpoints), which would leak the throwaway suggestion\n * thread. This handler runs the agent's event pipeline **without** that\n * persistence — no thread, lock, gateway, name-gen, or run telemetry — so\n * dynamic suggestions are side-effect-free in every runtime mode.\n *\n * The only per-request configuration it applies is forwarding the request's\n * allowlisted headers (`authorization` + `x-*`) onto the agent clone. It does\n * **not** attach any request middleware — no A2UI, no MCPApps, no\n * OpenGenerativeUI, no Intelligence enterprise-learning tools. The **client**\n * (the core suggestion engine) forces `toolChoice: copilotkitSuggest` in the\n * request body; this handler does not set tool choice itself and relies on it\n * being present in the incoming `input`. Given that forced tool choice, any\n * middleware-injected tools are dead weight, and MCPApps setup can incur a\n * `listTools` network round-trip per suggestion under `available: \"always\"` — a\n * side effect this path must never pay.\n *\n * When the client aborts the HTTP request (via its `AbortController`), the\n * server-side run is cancelled best-effort so an aborted suggestion does not\n * keep running a provider call to completion.\n *\n * @param params - The runtime, request, and resolved agent id.\n * @returns A `text/event-stream` `Response` of the provider agent's AG-UI events\n *   on success, or the resolution `Response` (e.g. 404/400) when agent/body\n *   resolution fails before streaming begins.\n */\nexport async function handleSuggestAgent({\n  runtime,\n  request,\n  agentId,\n}: SuggestAgentParameters): Promise<Response> {\n  const agent = await cloneAgentForRequest(runtime, agentId, request);\n  if (agent instanceof Response) {\n    return agent;\n  }\n\n  // Carry the registry key onto the clone so any framework relying on it during\n  // the run sees the correct agentId.\n  agent.agentId = agentId;\n\n  const input = await parseRunRequest(request);\n  if (input instanceof Response) {\n    return input;\n  }\n\n  // Forward eligible inbound headers onto the clone under the runtime's resolved\n  // forwarding policy (same helper the run handler uses: `authorization` /\n  // custom `x-*`, infra/proxy/platform headers denylisted, server-set headers\n  // winning on collision — #5712). Unlike a full agent run, the suggest path\n  // intentionally attaches no middleware (see the handler docblock): the forced\n  // `copilotkitSuggest` tool choice makes any middleware-injected tools dead\n  // weight, and MCPApps setup can trigger a `listTools` network round-trip we\n  // must never incur per suggestion.\n  //\n  // `AbstractAgent` doesn't declare `headers` on its base type (concrete\n  // framework agents add it), so narrow to the header-carrying shape rather\n  // than casting to `any`.\n  const headerCarryingAgent = agent as AbstractAgent & {\n    headers?: Record<string, string>;\n  };\n  headerCarryingAgent.headers = mergeForwardableHeaders(\n    headerCarryingAgent.headers,\n    request,\n    runtime.forwardHeadersPolicy ?? resolveForwardHeadersPolicy(undefined),\n  );\n\n  agent.setMessages(input.messages);\n  agent.setState(input.state);\n  agent.threadId = input.threadId;\n\n  // Stream the provider agent's events over SSE without persistence. This is\n  // the runner's event pipeline (`agent.runAgent({ onEvent })` + terminal-event\n  // finalization) minus the `GLOBAL_STORE` writes that would leak a thread.\n  // `captureTelemetry: false` keeps suggestions out of run telemetry; omitting\n  // `debugEventBus` keeps them out of the inspector's run trace.\n  return createSseEventResponse({\n    request,\n    agentId,\n    captureTelemetry: false,\n    observableFactory: () =>\n      new Observable<BaseEvent>((subscriber) => {\n        // Collected so `finalizeRunEvents` can append any missing terminal\n        // events (e.g. an unclosed message/tool call, or a `RUN_FINISHED`) —\n        // the same closure the runner applies — so the client sees a\n        // well-formed AG-UI sequence.\n        const collected: BaseEvent[] = [];\n        let settled = false;\n\n        void agent\n          .runAgent(input, {\n            onEvent: ({ event }) => {\n              collected.push(event);\n              subscriber.next(event);\n            },\n          })\n          .then(() => {\n            for (const event of finalizeRunEvents(collected, {\n              stopRequested: false,\n            })) {\n              subscriber.next(event);\n            }\n            settled = true;\n            subscriber.complete();\n          })\n          .catch((error: unknown) => {\n            settled = true;\n            subscriber.error(error);\n          });\n\n        // Teardown fires when the response stream is torn down (client abort /\n        // disconnect). Cancel the still-running provider call best-effort.\n        return () => {\n          if (!settled && typeof agent.abortRun === \"function\") {\n            try {\n              agent.abortRun();\n            } catch {\n              // best-effort — nothing actionable if aborting the run fails.\n            }\n          }\n        };\n      }),\n  });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,eAAsB,mBAAmB,EACvC,SACA,SACA,WAC4C;CAC5C,MAAM,QAAQ,MAAM,qBAAqB,SAAS,SAAS,QAAQ;AACnE,KAAI,iBAAiB,SACnB,QAAO;AAKT,OAAM,UAAU;CAEhB,MAAM,QAAQ,MAAM,gBAAgB,QAAQ;AAC5C,KAAI,iBAAiB,SACnB,QAAO;CAeT,MAAM,sBAAsB;AAG5B,qBAAoB,UAAU,wBAC5B,oBAAoB,SACpB,SACA,QAAQ,wBAAwB,4BAA4B,OAAU,CACvE;AAED,OAAM,YAAY,MAAM,SAAS;AACjC,OAAM,SAAS,MAAM,MAAM;AAC3B,OAAM,WAAW,MAAM;AAOvB,QAAO,uBAAuB;EAC5B;EACA;EACA,kBAAkB;EAClB,yBACE,IAAI,YAAuB,eAAe;GAKxC,MAAM,YAAyB,EAAE;GACjC,IAAI,UAAU;AAEd,GAAK,MACF,SAAS,OAAO,EACf,UAAU,EAAE,YAAY;AACtB,cAAU,KAAK,MAAM;AACrB,eAAW,KAAK,MAAM;MAEzB,CAAC,CACD,WAAW;AACV,SAAK,MAAM,SAAS,kBAAkB,WAAW,EAC/C,eAAe,OAChB,CAAC,CACA,YAAW,KAAK,MAAM;AAExB,cAAU;AACV,eAAW,UAAU;KACrB,CACD,OAAO,UAAmB;AACzB,cAAU;AACV,eAAW,MAAM,MAAM;KACvB;AAIJ,gBAAa;AACX,QAAI,CAAC,WAAW,OAAO,MAAM,aAAa,WACxC,KAAI;AACF,WAAM,UAAU;YACV;;IAKZ;EACL,CAAC"}