cortena-ui 1.16.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  Notable changes per release. Versions before 1.6.0 are recorded in the git log
4
4
  and in `../../CONSUMING.md`; this file starts where the changelog does.
5
5
 
6
+ ## 1.16.1
7
+
8
+ ### Fixed
9
+
10
+ - **The agent pop-up's control plane called the global `fetch` with an illegal
11
+ receiver, and had since 1.5.0.** Every call site is `this.fetch(...)`, and the
12
+ getter returned the unbound `globalThis.fetch`, so a native `fetch` refused it
13
+ with "Failed to execute 'fetch' on 'Window': Illegal invocation". The session
14
+ list threw on its first call and the pop-up rendered that sentence where the
15
+ conversation belongs — in every extension, in every product build. It survived
16
+ because every test injected a `fetchImpl` stub, and a plain function does not
17
+ care what `this` is. The stub is what hid it. The getter now binds, and the
18
+ test beside it uses a global that rejects a foreign receiver the way the real
19
+ one does.
20
+
6
21
  ## 1.16.0
7
22
 
8
23
  ### Fixed
@@ -85,8 +85,23 @@ var AguiAgentChatClient = class {
85
85
  "content-type": "application/json"
86
86
  };
87
87
  }
88
+ /**
89
+ * The global `fetch` is BOUND, and that is the whole point of this getter.
90
+ *
91
+ * Every call site below is `this.fetch(...)`, which invokes whatever this
92
+ * returns with `this` set to the client instance. A native `fetch` refuses
93
+ * that: "Failed to execute 'fetch' on 'Window': Illegal invocation". So the
94
+ * control plane — the session list, the history, abort and approval — threw
95
+ * on its first call in every product build, and the pop-up rendered that
96
+ * message where the conversation should be.
97
+ *
98
+ * It survived since 1.5.0 because the tests inject `fetchImpl`, and a plain
99
+ * function stub does not care what `this` is. The stub is exactly what hid
100
+ * the bug, so the test beside this change uses a `globalThis.fetch` that
101
+ * rejects a foreign `this` the way the real one does.
102
+ */
88
103
  get fetch() {
89
- return this.opts.fetchImpl ?? globalThis.fetch;
104
+ return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
90
105
  }
91
106
  /** `sessions.list` and `chat.history` over the REST control plane. */
92
107
  async request(method, params) {
@@ -1 +1 @@
1
- {"version":3,"file":"agui-client.js","names":[],"sources":["../../src/agent-chat/agui-client.ts"],"sourcesContent":["/**\n * `AgentChatClient` over AG-UI.\n *\n * Ported from `cortena-shared/src/agui/client.ts` (Cortena monorepo,\n * DESIGN-37). `cortena-shared` is not published, so what this package needs is\n * copied rather than imported; keep the two in step.\n *\n * **This is the only module in cortena-ui that imports `@ag-ui/client`**, and\n * `cortena-ui/agent-chat` is the only entry that reaches it. `@ag-ui/client`\n * brings rxjs, zod 3, uuid and protobuf; `test/bundle-probe.test.mjs` asserts\n * none of it is reachable from `cortena-ui` or `cortena-ui/core`.\n *\n * It is an OPTIONAL peer, and the import is therefore **lazy**: a static\n * `import { HttpAgent }` made the whole `cortena-ui/agent-chat` entry fail to\n * load for a consumer that had not installed it, so an extension on another\n * transport could not draw `AgentChatPopup` at all — a package it never asked\n * for decided whether its chat rendered. Loading it inside the factory keeps\n * the entry resolvable, and moves the failure to the one operation that\n * genuinely needs the peer, with a message that says what to install.\n *\n * What an extension must expose for this to work is in README.md, \"The agent\n * pop-up\": `POST /agui/run`, `/agui/abort`, `/agui/approval` and the two\n * control-plane reads, all proxied through the extension's own gateway with the\n * signed-in user's JWT — never a service token.\n *\n * Two things about the wire that are easy to get wrong:\n *\n * - The stream ends by **closing the connection** after `RUN_FINISHED` or\n * `RUN_ERROR`. There is no `data: [DONE]` sentinel; emitting one makes the\n * AG-UI client error the subject, because it `JSON.parse`s every `data:` line.\n * - Approvals go to their own endpoint. Sending a decision as `state` on a\n * second `/agui/run` opens a second concurrent stream, and two mappers then\n * write two sets of message ids into one bridge, which resets its text buffer\n * whenever the message id changes.\n */\n\nimport type { HttpAgent } from \"@ag-ui/client\";\nimport { AgentChatBridge } from \"./bridge\";\nimport type {\n AgentApprovalContext,\n AgentApprovalDecision,\n AgentApprovalEvent,\n AgentChatClient,\n AgentChatEvent,\n AgentChatSendInput,\n AgentToolEvent,\n} from \"./types\";\n\nexport interface CreateAguiAgentChatClientOptions {\n /**\n * Origin the AG-UI and control-plane paths hang off, no trailing slash.\n * For an extension this is its own gateway, which proxies to cortenacore.\n */\n baseUrl: string;\n /** The signed-in USER's JWT. Read on every request, never captured once. */\n getToken: () => string;\n /** The agent template slug — the extension id. Sessions are `agent:<id>:…`. */\n agentId: string;\n fetchImpl?: typeof fetch;\n /** Frontend tools advertised on every run; AG-UI requires a description. */\n tools?: Array<{ name: string; description: string; parameters?: unknown }>;\n /** Backoff before re-attaching a dropped stream. */\n reconnectDelays?: readonly number[];\n maxReconnectAttempts?: number;\n sleep?: (ms: number) => Promise<void>;\n now?: () => number;\n}\n\nconst DEFAULT_RECONNECT_DELAYS: readonly number[] = [500, 1_000, 2_000, 5_000, 10_000];\nconst DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;\n\nconst defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/**\n * What a relative `baseUrl` resolves against.\n *\n * The browser's own origin, and a placeholder under Node — where this module\n * is exercised by tests with a `fetchImpl` stub and there is no origin to\n * speak of. The placeholder never reaches the network: the stub sees the\n * resolved string, and `/api/core/...` is what it asserts on.\n */\nfunction requestBase(): string {\n return typeof location === \"undefined\" ? \"http://cortena.invalid\" : location.origin;\n}\n\n/** What `@ag-ui/client` exports and this module constructs. */\ntype HttpAgentConstructor = typeof import(\"@ag-ui/client\").HttpAgent;\n\nexport const MISSING_AGUI_PEER =\n 'cortena-ui/agent-chat: the AG-UI transport needs the optional peer \"@ag-ui/client\". ' +\n \"Install it (pnpm add @ag-ui/client), or pass your own AgentChatClient to AgentChat.\";\n\n/**\n * `@ag-ui/client`, on demand.\n *\n * The specifier is a literal so a bundler still sees the dependency and puts\n * it in a chunk of its own — `test/bundle-probe.test.mjs` measures that the\n * agent-chat entry carries it. What changes is *when* it is evaluated, and\n * what happens when it is not there at all.\n */\nasync function loadHttpAgent(): Promise<HttpAgentConstructor> {\n try {\n return (await import(\"@ag-ui/client\")).HttpAgent;\n } catch (cause) {\n throw new Error(MISSING_AGUI_PEER, { cause });\n }\n}\n\ntype Sink<T> = (event: T) => void;\n\ninterface ActiveRun {\n runId: string;\n sessionKey: string;\n bridge: AgentChatBridge;\n agent: HttpAgent;\n aborted: boolean;\n}\n\nclass AguiAgentChatClient implements AgentChatClient {\n readonly agentId: string;\n\n private readonly opts: CreateAguiAgentChatClientOptions;\n private readonly delays: readonly number[];\n private readonly maxAttempts: number;\n private readonly sleep: (ms: number) => Promise<void>;\n private readonly chatSinks = new Set<Sink<AgentChatEvent>>();\n private readonly toolSinks = new Set<Sink<AgentToolEvent>>();\n private readonly approvalSinks = new Set<Sink<AgentApprovalEvent>>();\n private active: ActiveRun | null = null;\n /** The optional peer, requested when the client is created and awaited per run. */\n private readonly httpAgent: Promise<HttpAgentConstructor>;\n\n constructor(opts: CreateAguiAgentChatClientOptions) {\n this.opts = opts;\n this.agentId = opts.agentId;\n this.delays = opts.reconnectDelays ?? DEFAULT_RECONNECT_DELAYS;\n this.maxAttempts = opts.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;\n this.sleep = opts.sleep ?? defaultSleep;\n // Started here so a missing peer is discovered when the client is made\n // rather than mid-stream. The rejection is answered immediately: a client\n // created and never sent to must not raise an unhandled rejection, and\n // `startRun` awaits the same promise where the user can be told.\n this.httpAgent = loadHttpAgent();\n this.httpAgent.catch(() => {});\n }\n\n onChat(sink: Sink<AgentChatEvent>): () => void {\n this.chatSinks.add(sink);\n return () => this.chatSinks.delete(sink);\n }\n\n onTool(sink: Sink<AgentToolEvent>): () => void {\n this.toolSinks.add(sink);\n return () => this.toolSinks.delete(sink);\n }\n\n onApproval(sink: Sink<AgentApprovalEvent>): () => void {\n this.approvalSinks.add(sink);\n return () => this.approvalSinks.delete(sink);\n }\n\n get runUrl(): string {\n return `${this.opts.baseUrl}/agui/run`;\n }\n\n get abortUrl(): string {\n return `${this.opts.baseUrl}/agui/abort`;\n }\n\n get approvalUrl(): string {\n return `${this.opts.baseUrl}/agui/approval`;\n }\n\n private headers(): Record<string, string> {\n return {\n authorization: `Bearer ${this.opts.getToken()}`,\n \"content-type\": \"application/json\",\n };\n }\n\n private get fetch(): typeof fetch {\n return this.opts.fetchImpl ?? globalThis.fetch;\n }\n\n /** `sessions.list` and `chat.history` over the REST control plane. */\n async request<T = unknown>(method: string, params: Record<string, unknown>): Promise<T> {\n const path = `${this.opts.baseUrl}/api/core/${method.split(\".\").join(\"/\")}`;\n const readOnly = method === \"sessions.list\" || method === \"chat.history\";\n // `new URL(path)` with no base throws on a relative `baseUrl`, and the one\n // in this module's own example is `\"/api/agent\"` — the shape every\n // extension uses, since the gateway that proxies these calls is the app's\n // own origin. `sessions.list` therefore threw before it was ever sent, and\n // the chat opened on \"The agent could not be reached\".\n const url = new URL(path, requestBase());\n if (readOnly) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n const response = await this.fetch(url.toString(), {\n method: readOnly ? \"GET\" : \"POST\",\n headers: this.headers(),\n ...(readOnly ? {} : { body: JSON.stringify(params) }),\n });\n if (!response.ok) {\n throw new Error(await readError(response, method));\n }\n return (await response.json()) as T;\n }\n\n async send(input: AgentChatSendInput): Promise<void> {\n // Fire and forget: the caller only awaits the acceptance. Events follow on\n // the stream, exactly as they do when the server acknowledges a send.\n void this.startRun(input);\n }\n\n private async startRun(input: AgentChatSendInput): Promise<void> {\n const bridge = new AgentChatBridge({\n runId: input.runId,\n sessionKey: input.sessionKey,\n chat: (event) => this.emit(this.chatSinks, event),\n tool: (event) => this.emit(this.toolSinks, event),\n approval: (event) => this.emit(this.approvalSinks, event),\n ...(this.opts.now ? { now: this.opts.now } : {}),\n });\n\n const forwarded: Record<string, unknown> = {\n ...(input.thinking ? { thinking: input.thinking } : {}),\n ...(input.attachments?.length ? { attachments: input.attachments } : {}),\n ...(input.model ? { model: input.model } : {}),\n // A click inside a rendered A2UI surface or MCP App. Structured rather\n // than typed prose: cortenacore turns it into the documented envelope.\n ...(input.a2uiAction ? { a2uiAction: input.a2uiAction } : {}),\n ...(input.mcpAppAction ? { mcpAppAction: input.mcpAppAction } : {}),\n };\n\n let Agent: HttpAgentConstructor;\n try {\n Agent = await this.httpAgent;\n } catch (err) {\n // The one operation that needs the peer, and the only place it can be\n // reported: in the chat, as a failed run, rather than as a rejected\n // promise nobody is holding.\n bridge.fail(err instanceof Error ? err.message : MISSING_AGUI_PEER);\n return;\n }\n\n let attempt = 0;\n let resume = false;\n for (;;) {\n const runId = resume ? `${input.runId}:resume-${attempt}` : input.runId;\n const agent = new Agent({\n url: this.runUrl,\n headers: this.headers(),\n threadId: input.sessionKey,\n // A re-attach carries no new user message; the server re-streams the\n // run named by forwardedProps.resumeRunId instead of starting one.\n initialMessages: resume\n ? []\n : [{ id: input.runId, role: \"user\", content: input.message }],\n ...(this.opts.fetchImpl ? { fetch: this.opts.fetchImpl } : {}),\n });\n this.active = {\n runId: input.runId,\n sessionKey: input.sessionKey,\n bridge,\n agent,\n aborted: this.active?.aborted === true && this.active.runId === input.runId,\n };\n try {\n await agent.runAgent(\n {\n runId,\n ...(this.opts.tools?.length ? { tools: this.opts.tools } : {}),\n forwardedProps: {\n ...forwarded,\n ...(resume ? { resumeRunId: input.runId } : {}),\n },\n },\n { onEvent: ({ event }) => bridge.handleEvent(event as never) },\n );\n return;\n } catch (err) {\n if (bridge.finished || this.active?.aborted) {\n return;\n }\n attempt += 1;\n if (attempt > this.maxAttempts) {\n bridge.fail(err instanceof Error ? err.message : \"The agent stream failed\");\n return;\n }\n await this.sleep(this.delays[Math.min(attempt - 1, this.delays.length - 1)] ?? 1_000);\n // The backoff is up to ten seconds long, and everything can change in\n // it: the user presses Stop, or a late RUN_FINISHED lands on the\n // bridge. Checking only before the sleep re-attached to a run that had\n // already ended — a second stream, a second set of message ids into\n // one bridge, and an abort the user watched do nothing.\n if (bridge.finished || this.active?.aborted) {\n return;\n }\n resume = true;\n }\n }\n }\n\n async abort(params?: { runId?: string; sessionKey?: string }): Promise<void> {\n const active = this.active;\n if (active) {\n active.aborted = true;\n try {\n active.agent.abortRun();\n } catch {\n // Already finished; the POST below is still worth making.\n }\n }\n const runId = params?.runId ?? active?.runId;\n const threadId = params?.sessionKey ?? active?.sessionKey;\n if (!runId || !threadId) {\n return;\n }\n try {\n await this.fetch(this.abortUrl, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify({ runId, threadId }),\n });\n } catch {\n // Best-effort: dropping the stream aborts the run server-side anyway.\n }\n }\n\n /**\n * Answer one approval, on the thread and run it arrived on.\n *\n * `this.active` is deliberately not consulted. An approval pauses a run, and\n * a user is free to open another session from the drawer or start another\n * turn while it waits — at which point `active.sessionKey` is a different\n * conversation and \"allow\" would be sent against it. The request carried its\n * own `{ runId, sessionKey }`; those are what go on the wire.\n */\n async resolveApproval(\n id: string,\n decision: AgentApprovalDecision,\n context: AgentApprovalContext,\n ): Promise<void> {\n const response = await this.fetch(this.approvalUrl, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify({\n id,\n decision,\n threadId: context.sessionKey,\n runId: context.runId,\n }),\n });\n // A rejected decision has to reach the user. Swallowing it left the card\n // gone, the run paused server-side and nothing on screen saying so — the\n // user believes they answered and the agent believes nobody did.\n if (!response.ok) {\n throw new Error(await readError(response, \"approval\"));\n }\n }\n\n private emit<T>(sinks: Set<Sink<T>>, event: T): void {\n for (const sink of [...sinks]) {\n sink(event);\n }\n }\n}\n\n/**\n * An `AgentChatClient` that streams over AG-UI and reads sessions and history\n * over the REST control plane.\n *\n * ```tsx\n * const client = React.useMemo(\n * () => createAguiAgentChatClient({ baseUrl: \"/api/agent\", getToken: () => jwt, agentId: \"tasks\" }),\n * [jwt],\n * );\n * ```\n */\nexport function createAguiAgentChatClient(\n options: CreateAguiAgentChatClientOptions,\n): AgentChatClient {\n return new AguiAgentChatClient(options);\n}\n\nasync function readError(response: Response, method: string): Promise<string> {\n try {\n const body = (await response.json()) as { error?: { message?: string } };\n if (body?.error?.message) {\n return body.error.message;\n }\n } catch {\n // Not JSON. The status is all there is.\n }\n if (response.status === 404) {\n // The single most common misconfiguration: the control-plane endpoints are\n // off, so every path under /api/core answers 404 before authentication.\n return `${method} is unavailable (404). Check that the AG-UI and control-plane endpoints are enabled.`;\n }\n return `${method} failed (${response.status})`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"agui-client.js","names":[],"sources":["../../src/agent-chat/agui-client.ts"],"sourcesContent":["/**\n * `AgentChatClient` over AG-UI.\n *\n * Ported from `cortena-shared/src/agui/client.ts` (Cortena monorepo,\n * DESIGN-37). `cortena-shared` is not published, so what this package needs is\n * copied rather than imported; keep the two in step.\n *\n * **This is the only module in cortena-ui that imports `@ag-ui/client`**, and\n * `cortena-ui/agent-chat` is the only entry that reaches it. `@ag-ui/client`\n * brings rxjs, zod 3, uuid and protobuf; `test/bundle-probe.test.mjs` asserts\n * none of it is reachable from `cortena-ui` or `cortena-ui/core`.\n *\n * It is an OPTIONAL peer, and the import is therefore **lazy**: a static\n * `import { HttpAgent }` made the whole `cortena-ui/agent-chat` entry fail to\n * load for a consumer that had not installed it, so an extension on another\n * transport could not draw `AgentChatPopup` at all — a package it never asked\n * for decided whether its chat rendered. Loading it inside the factory keeps\n * the entry resolvable, and moves the failure to the one operation that\n * genuinely needs the peer, with a message that says what to install.\n *\n * What an extension must expose for this to work is in README.md, \"The agent\n * pop-up\": `POST /agui/run`, `/agui/abort`, `/agui/approval` and the two\n * control-plane reads, all proxied through the extension's own gateway with the\n * signed-in user's JWT — never a service token.\n *\n * Two things about the wire that are easy to get wrong:\n *\n * - The stream ends by **closing the connection** after `RUN_FINISHED` or\n * `RUN_ERROR`. There is no `data: [DONE]` sentinel; emitting one makes the\n * AG-UI client error the subject, because it `JSON.parse`s every `data:` line.\n * - Approvals go to their own endpoint. Sending a decision as `state` on a\n * second `/agui/run` opens a second concurrent stream, and two mappers then\n * write two sets of message ids into one bridge, which resets its text buffer\n * whenever the message id changes.\n */\n\nimport type { HttpAgent } from \"@ag-ui/client\";\nimport { AgentChatBridge } from \"./bridge\";\nimport type {\n AgentApprovalContext,\n AgentApprovalDecision,\n AgentApprovalEvent,\n AgentChatClient,\n AgentChatEvent,\n AgentChatSendInput,\n AgentToolEvent,\n} from \"./types\";\n\nexport interface CreateAguiAgentChatClientOptions {\n /**\n * Origin the AG-UI and control-plane paths hang off, no trailing slash.\n * For an extension this is its own gateway, which proxies to cortenacore.\n */\n baseUrl: string;\n /** The signed-in USER's JWT. Read on every request, never captured once. */\n getToken: () => string;\n /** The agent template slug — the extension id. Sessions are `agent:<id>:…`. */\n agentId: string;\n fetchImpl?: typeof fetch;\n /** Frontend tools advertised on every run; AG-UI requires a description. */\n tools?: Array<{ name: string; description: string; parameters?: unknown }>;\n /** Backoff before re-attaching a dropped stream. */\n reconnectDelays?: readonly number[];\n maxReconnectAttempts?: number;\n sleep?: (ms: number) => Promise<void>;\n now?: () => number;\n}\n\nconst DEFAULT_RECONNECT_DELAYS: readonly number[] = [500, 1_000, 2_000, 5_000, 10_000];\nconst DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;\n\nconst defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/**\n * What a relative `baseUrl` resolves against.\n *\n * The browser's own origin, and a placeholder under Node — where this module\n * is exercised by tests with a `fetchImpl` stub and there is no origin to\n * speak of. The placeholder never reaches the network: the stub sees the\n * resolved string, and `/api/core/...` is what it asserts on.\n */\nfunction requestBase(): string {\n return typeof location === \"undefined\" ? \"http://cortena.invalid\" : location.origin;\n}\n\n/** What `@ag-ui/client` exports and this module constructs. */\ntype HttpAgentConstructor = typeof import(\"@ag-ui/client\").HttpAgent;\n\nexport const MISSING_AGUI_PEER =\n 'cortena-ui/agent-chat: the AG-UI transport needs the optional peer \"@ag-ui/client\". ' +\n \"Install it (pnpm add @ag-ui/client), or pass your own AgentChatClient to AgentChat.\";\n\n/**\n * `@ag-ui/client`, on demand.\n *\n * The specifier is a literal so a bundler still sees the dependency and puts\n * it in a chunk of its own — `test/bundle-probe.test.mjs` measures that the\n * agent-chat entry carries it. What changes is *when* it is evaluated, and\n * what happens when it is not there at all.\n */\nasync function loadHttpAgent(): Promise<HttpAgentConstructor> {\n try {\n return (await import(\"@ag-ui/client\")).HttpAgent;\n } catch (cause) {\n throw new Error(MISSING_AGUI_PEER, { cause });\n }\n}\n\ntype Sink<T> = (event: T) => void;\n\ninterface ActiveRun {\n runId: string;\n sessionKey: string;\n bridge: AgentChatBridge;\n agent: HttpAgent;\n aborted: boolean;\n}\n\nclass AguiAgentChatClient implements AgentChatClient {\n readonly agentId: string;\n\n private readonly opts: CreateAguiAgentChatClientOptions;\n private readonly delays: readonly number[];\n private readonly maxAttempts: number;\n private readonly sleep: (ms: number) => Promise<void>;\n private readonly chatSinks = new Set<Sink<AgentChatEvent>>();\n private readonly toolSinks = new Set<Sink<AgentToolEvent>>();\n private readonly approvalSinks = new Set<Sink<AgentApprovalEvent>>();\n private active: ActiveRun | null = null;\n /** The optional peer, requested when the client is created and awaited per run. */\n private readonly httpAgent: Promise<HttpAgentConstructor>;\n\n constructor(opts: CreateAguiAgentChatClientOptions) {\n this.opts = opts;\n this.agentId = opts.agentId;\n this.delays = opts.reconnectDelays ?? DEFAULT_RECONNECT_DELAYS;\n this.maxAttempts = opts.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;\n this.sleep = opts.sleep ?? defaultSleep;\n // Started here so a missing peer is discovered when the client is made\n // rather than mid-stream. The rejection is answered immediately: a client\n // created and never sent to must not raise an unhandled rejection, and\n // `startRun` awaits the same promise where the user can be told.\n this.httpAgent = loadHttpAgent();\n this.httpAgent.catch(() => {});\n }\n\n onChat(sink: Sink<AgentChatEvent>): () => void {\n this.chatSinks.add(sink);\n return () => this.chatSinks.delete(sink);\n }\n\n onTool(sink: Sink<AgentToolEvent>): () => void {\n this.toolSinks.add(sink);\n return () => this.toolSinks.delete(sink);\n }\n\n onApproval(sink: Sink<AgentApprovalEvent>): () => void {\n this.approvalSinks.add(sink);\n return () => this.approvalSinks.delete(sink);\n }\n\n get runUrl(): string {\n return `${this.opts.baseUrl}/agui/run`;\n }\n\n get abortUrl(): string {\n return `${this.opts.baseUrl}/agui/abort`;\n }\n\n get approvalUrl(): string {\n return `${this.opts.baseUrl}/agui/approval`;\n }\n\n private headers(): Record<string, string> {\n return {\n authorization: `Bearer ${this.opts.getToken()}`,\n \"content-type\": \"application/json\",\n };\n }\n\n /**\n * The global `fetch` is BOUND, and that is the whole point of this getter.\n *\n * Every call site below is `this.fetch(...)`, which invokes whatever this\n * returns with `this` set to the client instance. A native `fetch` refuses\n * that: \"Failed to execute 'fetch' on 'Window': Illegal invocation\". So the\n * control plane — the session list, the history, abort and approval — threw\n * on its first call in every product build, and the pop-up rendered that\n * message where the conversation should be.\n *\n * It survived since 1.5.0 because the tests inject `fetchImpl`, and a plain\n * function stub does not care what `this` is. The stub is exactly what hid\n * the bug, so the test beside this change uses a `globalThis.fetch` that\n * rejects a foreign `this` the way the real one does.\n */\n private get fetch(): typeof fetch {\n return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);\n }\n\n /** `sessions.list` and `chat.history` over the REST control plane. */\n async request<T = unknown>(method: string, params: Record<string, unknown>): Promise<T> {\n const path = `${this.opts.baseUrl}/api/core/${method.split(\".\").join(\"/\")}`;\n const readOnly = method === \"sessions.list\" || method === \"chat.history\";\n // `new URL(path)` with no base throws on a relative `baseUrl`, and the one\n // in this module's own example is `\"/api/agent\"` — the shape every\n // extension uses, since the gateway that proxies these calls is the app's\n // own origin. `sessions.list` therefore threw before it was ever sent, and\n // the chat opened on \"The agent could not be reached\".\n const url = new URL(path, requestBase());\n if (readOnly) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n const response = await this.fetch(url.toString(), {\n method: readOnly ? \"GET\" : \"POST\",\n headers: this.headers(),\n ...(readOnly ? {} : { body: JSON.stringify(params) }),\n });\n if (!response.ok) {\n throw new Error(await readError(response, method));\n }\n return (await response.json()) as T;\n }\n\n async send(input: AgentChatSendInput): Promise<void> {\n // Fire and forget: the caller only awaits the acceptance. Events follow on\n // the stream, exactly as they do when the server acknowledges a send.\n void this.startRun(input);\n }\n\n private async startRun(input: AgentChatSendInput): Promise<void> {\n const bridge = new AgentChatBridge({\n runId: input.runId,\n sessionKey: input.sessionKey,\n chat: (event) => this.emit(this.chatSinks, event),\n tool: (event) => this.emit(this.toolSinks, event),\n approval: (event) => this.emit(this.approvalSinks, event),\n ...(this.opts.now ? { now: this.opts.now } : {}),\n });\n\n const forwarded: Record<string, unknown> = {\n ...(input.thinking ? { thinking: input.thinking } : {}),\n ...(input.attachments?.length ? { attachments: input.attachments } : {}),\n ...(input.model ? { model: input.model } : {}),\n // A click inside a rendered A2UI surface or MCP App. Structured rather\n // than typed prose: cortenacore turns it into the documented envelope.\n ...(input.a2uiAction ? { a2uiAction: input.a2uiAction } : {}),\n ...(input.mcpAppAction ? { mcpAppAction: input.mcpAppAction } : {}),\n };\n\n let Agent: HttpAgentConstructor;\n try {\n Agent = await this.httpAgent;\n } catch (err) {\n // The one operation that needs the peer, and the only place it can be\n // reported: in the chat, as a failed run, rather than as a rejected\n // promise nobody is holding.\n bridge.fail(err instanceof Error ? err.message : MISSING_AGUI_PEER);\n return;\n }\n\n let attempt = 0;\n let resume = false;\n for (;;) {\n const runId = resume ? `${input.runId}:resume-${attempt}` : input.runId;\n const agent = new Agent({\n url: this.runUrl,\n headers: this.headers(),\n threadId: input.sessionKey,\n // A re-attach carries no new user message; the server re-streams the\n // run named by forwardedProps.resumeRunId instead of starting one.\n initialMessages: resume\n ? []\n : [{ id: input.runId, role: \"user\", content: input.message }],\n ...(this.opts.fetchImpl ? { fetch: this.opts.fetchImpl } : {}),\n });\n this.active = {\n runId: input.runId,\n sessionKey: input.sessionKey,\n bridge,\n agent,\n aborted: this.active?.aborted === true && this.active.runId === input.runId,\n };\n try {\n await agent.runAgent(\n {\n runId,\n ...(this.opts.tools?.length ? { tools: this.opts.tools } : {}),\n forwardedProps: {\n ...forwarded,\n ...(resume ? { resumeRunId: input.runId } : {}),\n },\n },\n { onEvent: ({ event }) => bridge.handleEvent(event as never) },\n );\n return;\n } catch (err) {\n if (bridge.finished || this.active?.aborted) {\n return;\n }\n attempt += 1;\n if (attempt > this.maxAttempts) {\n bridge.fail(err instanceof Error ? err.message : \"The agent stream failed\");\n return;\n }\n await this.sleep(this.delays[Math.min(attempt - 1, this.delays.length - 1)] ?? 1_000);\n // The backoff is up to ten seconds long, and everything can change in\n // it: the user presses Stop, or a late RUN_FINISHED lands on the\n // bridge. Checking only before the sleep re-attached to a run that had\n // already ended — a second stream, a second set of message ids into\n // one bridge, and an abort the user watched do nothing.\n if (bridge.finished || this.active?.aborted) {\n return;\n }\n resume = true;\n }\n }\n }\n\n async abort(params?: { runId?: string; sessionKey?: string }): Promise<void> {\n const active = this.active;\n if (active) {\n active.aborted = true;\n try {\n active.agent.abortRun();\n } catch {\n // Already finished; the POST below is still worth making.\n }\n }\n const runId = params?.runId ?? active?.runId;\n const threadId = params?.sessionKey ?? active?.sessionKey;\n if (!runId || !threadId) {\n return;\n }\n try {\n await this.fetch(this.abortUrl, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify({ runId, threadId }),\n });\n } catch {\n // Best-effort: dropping the stream aborts the run server-side anyway.\n }\n }\n\n /**\n * Answer one approval, on the thread and run it arrived on.\n *\n * `this.active` is deliberately not consulted. An approval pauses a run, and\n * a user is free to open another session from the drawer or start another\n * turn while it waits — at which point `active.sessionKey` is a different\n * conversation and \"allow\" would be sent against it. The request carried its\n * own `{ runId, sessionKey }`; those are what go on the wire.\n */\n async resolveApproval(\n id: string,\n decision: AgentApprovalDecision,\n context: AgentApprovalContext,\n ): Promise<void> {\n const response = await this.fetch(this.approvalUrl, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify({\n id,\n decision,\n threadId: context.sessionKey,\n runId: context.runId,\n }),\n });\n // A rejected decision has to reach the user. Swallowing it left the card\n // gone, the run paused server-side and nothing on screen saying so — the\n // user believes they answered and the agent believes nobody did.\n if (!response.ok) {\n throw new Error(await readError(response, \"approval\"));\n }\n }\n\n private emit<T>(sinks: Set<Sink<T>>, event: T): void {\n for (const sink of [...sinks]) {\n sink(event);\n }\n }\n}\n\n/**\n * An `AgentChatClient` that streams over AG-UI and reads sessions and history\n * over the REST control plane.\n *\n * ```tsx\n * const client = React.useMemo(\n * () => createAguiAgentChatClient({ baseUrl: \"/api/agent\", getToken: () => jwt, agentId: \"tasks\" }),\n * [jwt],\n * );\n * ```\n */\nexport function createAguiAgentChatClient(\n options: CreateAguiAgentChatClientOptions,\n): AgentChatClient {\n return new AguiAgentChatClient(options);\n}\n\nasync function readError(response: Response, method: string): Promise<string> {\n try {\n const body = (await response.json()) as { error?: { message?: string } };\n if (body?.error?.message) {\n return body.error.message;\n }\n } catch {\n // Not JSON. The status is all there is.\n }\n if (response.status === 404) {\n // The single most common misconfiguration: the control-plane endpoints are\n // off, so every path under /api/core answers 404 before authentication.\n return `${method} is unavailable (404). Check that the AG-UI and control-plane endpoints are enabled.`;\n }\n return `${method} failed (${response.status})`;\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { VariantProps } from "class-variance-authority";
3
3
  import * as React from "react";
4
4
  //#region src/components/badge.d.ts
5
5
  declare const badgeVariants: (props?: ({
6
- variant?: "default" | "success" | "warning" | "warn" | "destructive" | "danger" | "info" | "neutral" | "secondary" | "outline" | null | undefined;
6
+ variant?: "default" | "success" | "warn" | "destructive" | "danger" | "info" | "warning" | "neutral" | "secondary" | "outline" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  export interface BadgeProps extends React.ComponentProps<"span">, VariantProps<typeof badgeVariants> {}
9
9
  declare function Badge({ className, variant, ...props }: BadgeProps): React.JSX.Element;
@@ -21,7 +21,7 @@ import { Toast } from "@base-ui/react/toast";
21
21
  * `toastManager` to `Toaster` when you need to fire toasts outside React.
22
22
  */
23
23
  declare const toastVariants: (props?: ({
24
- variant?: "default" | "success" | "warning" | "destructive" | null | undefined;
24
+ variant?: "default" | "success" | "destructive" | "warning" | null | undefined;
25
25
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
26
26
  /** The Base UI toast manager: `{ toasts, add, close, update, promise }`. */
27
27
  declare const useToast: typeof Toast.useToastManager;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cortena-ui",
3
- "version": "1.16.0",
4
- "description": "Cortena UI — the shared React component library for cortenaweb and every Cortena extension. shadcn over Base UI, styled entirely from cortena-design tokens.",
3
+ "version": "1.16.1",
4
+ "description": "Cortena UI \u2014 the shared React component library for cortenaweb and every Cortena extension. shadcn over Base UI, styled entirely from cortena-design tokens.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
7
7
  "type": "git",
@@ -178,8 +178,23 @@ class AguiAgentChatClient implements AgentChatClient {
178
178
  };
179
179
  }
180
180
 
181
+ /**
182
+ * The global `fetch` is BOUND, and that is the whole point of this getter.
183
+ *
184
+ * Every call site below is `this.fetch(...)`, which invokes whatever this
185
+ * returns with `this` set to the client instance. A native `fetch` refuses
186
+ * that: "Failed to execute 'fetch' on 'Window': Illegal invocation". So the
187
+ * control plane — the session list, the history, abort and approval — threw
188
+ * on its first call in every product build, and the pop-up rendered that
189
+ * message where the conversation should be.
190
+ *
191
+ * It survived since 1.5.0 because the tests inject `fetchImpl`, and a plain
192
+ * function stub does not care what `this` is. The stub is exactly what hid
193
+ * the bug, so the test beside this change uses a `globalThis.fetch` that
194
+ * rejects a foreign `this` the way the real one does.
195
+ */
181
196
  private get fetch(): typeof fetch {
182
- return this.opts.fetchImpl ?? globalThis.fetch;
197
+ return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
183
198
  }
184
199
 
185
200
  /** `sessions.list` and `chat.history` over the REST control plane. */