cortena-ui 1.4.2 → 1.5.0

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.
Files changed (133) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +149 -1
  3. package/dist/a2ui/views.js +2 -2
  4. package/dist/agent-chat/a2ui-block.d.ts +60 -0
  5. package/dist/agent-chat/a2ui-block.js +69 -0
  6. package/dist/agent-chat/a2ui-block.js.map +1 -0
  7. package/dist/agent-chat/agui-client.d.ts +40 -0
  8. package/dist/agent-chat/agui-client.js +251 -0
  9. package/dist/agent-chat/agui-client.js.map +1 -0
  10. package/dist/agent-chat/bridge.d.ts +109 -0
  11. package/dist/agent-chat/bridge.js +349 -0
  12. package/dist/agent-chat/bridge.js.map +1 -0
  13. package/dist/agent-chat/session.d.ts +58 -0
  14. package/dist/agent-chat/session.js +249 -0
  15. package/dist/agent-chat/session.js.map +1 -0
  16. package/dist/agent-chat/store.d.ts +67 -0
  17. package/dist/agent-chat/store.js +548 -0
  18. package/dist/agent-chat/store.js.map +1 -0
  19. package/dist/agent-chat/types.d.ts +187 -0
  20. package/dist/agent-chat/types.js +17 -0
  21. package/dist/agent-chat/types.js.map +1 -0
  22. package/dist/agent-chat.d.ts +10 -0
  23. package/dist/agent-chat.js +10 -0
  24. package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
  25. package/dist/components/admin-permissions/admin-permissions.js +101 -0
  26. package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
  27. package/dist/components/admin-permissions/context.d.ts +70 -0
  28. package/dist/components/admin-permissions/context.js +258 -0
  29. package/dist/components/admin-permissions/context.js.map +1 -0
  30. package/dist/components/admin-permissions/index.d.ts +10 -0
  31. package/dist/components/admin-permissions/licence.d.ts +15 -0
  32. package/dist/components/admin-permissions/licence.js +78 -0
  33. package/dist/components/admin-permissions/licence.js.map +1 -0
  34. package/dist/components/admin-permissions/matrix.d.ts +20 -0
  35. package/dist/components/admin-permissions/matrix.js +191 -0
  36. package/dist/components/admin-permissions/matrix.js.map +1 -0
  37. package/dist/components/admin-permissions/members.d.ts +18 -0
  38. package/dist/components/admin-permissions/members.js +185 -0
  39. package/dist/components/admin-permissions/members.js.map +1 -0
  40. package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
  41. package/dist/components/admin-permissions/role-assignment.js +174 -0
  42. package/dist/components/admin-permissions/role-assignment.js.map +1 -0
  43. package/dist/components/admin-permissions/roles.d.ts +25 -0
  44. package/dist/components/admin-permissions/roles.js +168 -0
  45. package/dist/components/admin-permissions/roles.js.map +1 -0
  46. package/dist/components/admin-permissions/types.d.ts +152 -0
  47. package/dist/components/admin-permissions/types.js +63 -0
  48. package/dist/components/admin-permissions/types.js.map +1 -0
  49. package/dist/components/agent-chat-popup.d.ts +29 -0
  50. package/dist/components/agent-chat-popup.js +188 -0
  51. package/dist/components/agent-chat-popup.js.map +1 -0
  52. package/dist/components/agent-chat.d.ts +143 -0
  53. package/dist/components/agent-chat.js +578 -0
  54. package/dist/components/agent-chat.js.map +1 -0
  55. package/dist/components/app-shell.d.ts +126 -0
  56. package/dist/components/app-shell.js +297 -0
  57. package/dist/components/app-shell.js.map +1 -0
  58. package/dist/components/badge.d.ts +1 -1
  59. package/dist/components/button-link.js +1 -1
  60. package/dist/components/button.d.ts +2 -2
  61. package/dist/components/checkbox.d.ts +1 -1
  62. package/dist/components/combobox.d.ts +1 -1
  63. package/dist/components/combobox.js +1 -1
  64. package/dist/components/consent-screen.d.ts +65 -0
  65. package/dist/components/consent-screen.js +123 -0
  66. package/dist/components/consent-screen.js.map +1 -0
  67. package/dist/components/data-table/data-table.d.ts +15 -1
  68. package/dist/components/data-table/data-table.js +18 -4
  69. package/dist/components/data-table/data-table.js.map +1 -1
  70. package/dist/components/data-table/index.d.ts +4 -4
  71. package/dist/components/data-table/parts.d.ts +27 -3
  72. package/dist/components/data-table/parts.js +175 -55
  73. package/dist/components/data-table/parts.js.map +1 -1
  74. package/dist/components/data-table/types.d.ts +61 -0
  75. package/dist/components/data-table/use-data-table.js +91 -6
  76. package/dist/components/data-table/use-data-table.js.map +1 -1
  77. package/dist/components/data-table/use-server-source.js +119 -28
  78. package/dist/components/data-table/use-server-source.js.map +1 -1
  79. package/dist/components/help-panel.d.ts +131 -0
  80. package/dist/components/help-panel.js +545 -0
  81. package/dist/components/help-panel.js.map +1 -0
  82. package/dist/components/login-screen.d.ts +127 -0
  83. package/dist/components/login-screen.js +339 -0
  84. package/dist/components/login-screen.js.map +1 -0
  85. package/dist/components/session-guard.d.ts +268 -0
  86. package/dist/components/session-guard.js +632 -0
  87. package/dist/components/session-guard.js.map +1 -0
  88. package/dist/components/toast.d.ts +1 -1
  89. package/dist/core.d.ts +5 -1
  90. package/dist/core.js +11 -7
  91. package/dist/data-table.d.ts +13 -4
  92. package/dist/data-table.js +10 -2
  93. package/dist/hooks/use-cortena-theme.js +49 -3
  94. package/dist/hooks/use-cortena-theme.js.map +1 -1
  95. package/dist/index.d.ts +17 -4
  96. package/dist/index.js +21 -8
  97. package/dist/markdown.d.ts +2 -1
  98. package/dist/markdown.js +2 -1
  99. package/package.json +16 -4
  100. package/src/agent-chat/a2ui-block.ts +118 -0
  101. package/src/agent-chat/agui-client.ts +405 -0
  102. package/src/agent-chat/bridge.ts +433 -0
  103. package/src/agent-chat/session.ts +392 -0
  104. package/src/agent-chat/store.ts +738 -0
  105. package/src/agent-chat/types.ts +213 -0
  106. package/src/components/admin-permissions/admin-permissions.tsx +130 -0
  107. package/src/components/admin-permissions/context.tsx +376 -0
  108. package/src/components/admin-permissions/index.tsx +32 -0
  109. package/src/components/admin-permissions/licence.tsx +84 -0
  110. package/src/components/admin-permissions/matrix.tsx +257 -0
  111. package/src/components/admin-permissions/members.tsx +204 -0
  112. package/src/components/admin-permissions/role-assignment.tsx +239 -0
  113. package/src/components/admin-permissions/roles.tsx +169 -0
  114. package/src/components/admin-permissions/types.ts +231 -0
  115. package/src/components/agent-chat-popup.tsx +289 -0
  116. package/src/components/agent-chat.tsx +843 -0
  117. package/src/components/app-shell.tsx +502 -0
  118. package/src/components/consent-screen.tsx +239 -0
  119. package/src/components/data-table/data-table.tsx +36 -0
  120. package/src/components/data-table/index.tsx +6 -1
  121. package/src/components/data-table/parts.tsx +223 -47
  122. package/src/components/data-table/types.ts +68 -0
  123. package/src/components/data-table/use-data-table.ts +152 -4
  124. package/src/components/data-table/use-server-source.ts +150 -12
  125. package/src/components/help-panel.tsx +765 -0
  126. package/src/components/login-screen.tsx +479 -0
  127. package/src/components/session-guard.tsx +1071 -0
  128. package/src/entries/agent-chat.ts +113 -0
  129. package/src/entries/core.ts +8 -0
  130. package/src/entries/data-table.ts +41 -0
  131. package/src/entries/markdown.ts +25 -0
  132. package/src/hooks/use-cortena-theme.ts +63 -4
  133. package/src/index.ts +6 -0
@@ -0,0 +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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"}
@@ -0,0 +1,109 @@
1
+ "use client";
2
+ import { AgentApprovalEvent, AgentChatEvent, AgentToolEvent } from "./types.js";
3
+ //#region src/agent-chat/bridge.d.ts
4
+ /** The subset of an AG-UI event this bridge reads. */
5
+ export interface AguiIncomingEvent {
6
+ type: string;
7
+ messageId?: string;
8
+ delta?: string;
9
+ toolCallId?: string;
10
+ toolCallName?: string;
11
+ content?: string;
12
+ message?: string;
13
+ name?: string;
14
+ value?: unknown;
15
+ metadata?: Record<string, unknown>;
16
+ snapshot?: unknown;
17
+ outcome?: {
18
+ type?: string;
19
+ interrupts?: Array<Record<string, unknown>>;
20
+ };
21
+ }
22
+ /** `CUSTOM` event names cortenacore emits that this bridge acts on. */
23
+ export declare const AGUI_CUSTOM_NAMES: {
24
+ readonly CHAT_ABORTED: "cortena.chat.aborted";
25
+ readonly TOOL_PROGRESS: "cortena.tool.progress";
26
+ readonly APPROVAL_REQUEST: "cortena.approval.request";
27
+ readonly APPROVAL_RESOLVED: "cortena.approval.resolved";
28
+ readonly A2UI: "cortena.a2ui";
29
+ };
30
+ export interface AgentChatBridgeOptions {
31
+ runId: string;
32
+ sessionKey: string;
33
+ chat: (event: AgentChatEvent) => void;
34
+ tool?: (event: AgentToolEvent) => void;
35
+ approval?: (event: AgentApprovalEvent) => void;
36
+ now?: () => number;
37
+ }
38
+ export declare class AgentChatBridge {
39
+ readonly runId: string;
40
+ readonly sessionKey: string;
41
+ private readonly chat;
42
+ private readonly tool;
43
+ private readonly approval;
44
+ private readonly now;
45
+ private seq;
46
+ /** Text of the assistant message currently streaming. */
47
+ private text;
48
+ private textMessageId;
49
+ private reasoning;
50
+ /**
51
+ * The reasoning accumulated before each assistant message began, by message
52
+ * id, and the whole of the re-attach defence for the chain of thought.
53
+ *
54
+ * A re-attach re-streams the run from the top, so every REASONING delta
55
+ * arrives a second time. Restoring this snapshot when a message id starts
56
+ * again rewinds the buffer to exactly the point the first pass reached,
57
+ * whichever side of `TEXT_MESSAGE_START` the reasoning arrived on.
58
+ */
59
+ private readonly reasoningAtStart;
60
+ /**
61
+ * A2UI blocks seen this run. The chat treats the content array as a
62
+ * snapshot, so every emission carries all of them — the same rule the text
63
+ * follows, and what makes a re-attach land in the same place.
64
+ */
65
+ private a2ui;
66
+ /** Accumulated `TOOL_CALL_ARGS` deltas, per tool call id. */
67
+ private readonly args;
68
+ /**
69
+ * A2UI messages already folded onto each surface, by their serialised form.
70
+ *
71
+ * A re-attach re-streams the run from the beginning, so every
72
+ * `CUSTOM cortena.a2ui` arrives again. Folding concatenates, so without this
73
+ * the surface's message list doubled on the first reconnection and trebled
74
+ * on the second — a table drawn once, then drawn again underneath itself.
75
+ */
76
+ private readonly seenA2ui;
77
+ private terminal;
78
+ private readonly pendingInterrupts;
79
+ constructor(opts: AgentChatBridgeOptions);
80
+ /** True once RUN_FINISHED or RUN_ERROR has been seen; no retry after this. */
81
+ get finished(): boolean;
82
+ /** Interrupts carried by the terminal RUN_FINISHED, if any. */
83
+ get interrupts(): ReadonlyArray<Record<string, unknown>>;
84
+ private blocks;
85
+ private emitDelta;
86
+ private emitFinal;
87
+ /** Surface a transport failure the way a run error is surfaced. */
88
+ fail(message: string): void;
89
+ handleEvent(event: AguiIncomingEvent): void;
90
+ /**
91
+ * Accumulate one `TOOL_CALL_ARGS` delta and publish the best reading of it.
92
+ *
93
+ * Published on every delta rather than only at `TOOL_CALL_END`, because a
94
+ * run that is interrupted between the two — an approval, an abort, a dropped
95
+ * stream — would otherwise show a tool card with no input at all, which is
96
+ * the state the card exists to avoid.
97
+ */
98
+ private accumulateArgs;
99
+ private flushArgs;
100
+ private publishArgs;
101
+ /**
102
+ * The block with any message this surface has already been sent removed;
103
+ * `null` when nothing in it is new.
104
+ */
105
+ private freshA2UI;
106
+ private handleCustom;
107
+ }
108
+ //#endregion
109
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1,349 @@
1
+ "use client";
2
+ import { foldA2UIBlocks, readA2UIBlock } from "./a2ui-block.js";
3
+ //#region src/agent-chat/bridge.ts
4
+ /**
5
+ * AG-UI events -> the three `AgentChatClient` sinks.
6
+ *
7
+ * Ported from `cortena-shared/src/agui/bridge.ts` (Cortena monorepo, DESIGN-37
8
+ * step 1). `cortena-shared` is not published, so the translation lives here;
9
+ * keep the two in step. It knows nothing about HTTP or `@ag-ui/client`, which
10
+ * is why the tests can drive it with a literal array of events.
11
+ *
12
+ * One change against the original, and it is the reason this file is not a
13
+ * copy: **TOOL_CALL_ARGS is handled.** cortenacore's mapper emits
14
+ * `TOOL_CALL_START` -> `TOOL_CALL_ARGS { toolCallId, delta }` -> `TOOL_CALL_END`
15
+ * at tool-start time, with the whole argument object stringified into one
16
+ * `delta`. The original bridge had no case for it, so every tool card in the
17
+ * AG-UI path showed a name and a spinner and never showed what the tool was
18
+ * called with. The deltas are accumulated per call id and parsed when they
19
+ * form valid JSON, which is also correct for a transport that streams the
20
+ * arguments a fragment at a time.
21
+ */
22
+ /** `CUSTOM` event names cortenacore emits that this bridge acts on. */
23
+ const AGUI_CUSTOM_NAMES = {
24
+ CHAT_ABORTED: "cortena.chat.aborted",
25
+ TOOL_PROGRESS: "cortena.tool.progress",
26
+ APPROVAL_REQUEST: "cortena.approval.request",
27
+ APPROVAL_RESOLVED: "cortena.approval.resolved",
28
+ A2UI: "cortena.a2ui"
29
+ };
30
+ const DECISIONS = [
31
+ "allow-once",
32
+ "allow-always",
33
+ "deny"
34
+ ];
35
+ var AgentChatBridge = class {
36
+ runId;
37
+ sessionKey;
38
+ chat;
39
+ tool;
40
+ approval;
41
+ now;
42
+ seq = 0;
43
+ /** Text of the assistant message currently streaming. */
44
+ text = "";
45
+ textMessageId = null;
46
+ reasoning = "";
47
+ /**
48
+ * The reasoning accumulated before each assistant message began, by message
49
+ * id, and the whole of the re-attach defence for the chain of thought.
50
+ *
51
+ * A re-attach re-streams the run from the top, so every REASONING delta
52
+ * arrives a second time. Restoring this snapshot when a message id starts
53
+ * again rewinds the buffer to exactly the point the first pass reached,
54
+ * whichever side of `TEXT_MESSAGE_START` the reasoning arrived on.
55
+ */
56
+ reasoningAtStart = /* @__PURE__ */ new Map();
57
+ /**
58
+ * A2UI blocks seen this run. The chat treats the content array as a
59
+ * snapshot, so every emission carries all of them — the same rule the text
60
+ * follows, and what makes a re-attach land in the same place.
61
+ */
62
+ a2ui = [];
63
+ /** Accumulated `TOOL_CALL_ARGS` deltas, per tool call id. */
64
+ args = /* @__PURE__ */ new Map();
65
+ /**
66
+ * A2UI messages already folded onto each surface, by their serialised form.
67
+ *
68
+ * A re-attach re-streams the run from the beginning, so every
69
+ * `CUSTOM cortena.a2ui` arrives again. Folding concatenates, so without this
70
+ * the surface's message list doubled on the first reconnection and trebled
71
+ * on the second — a table drawn once, then drawn again underneath itself.
72
+ */
73
+ seenA2ui = /* @__PURE__ */ new Map();
74
+ terminal = false;
75
+ pendingInterrupts = [];
76
+ constructor(opts) {
77
+ this.runId = opts.runId;
78
+ this.sessionKey = opts.sessionKey;
79
+ this.chat = opts.chat;
80
+ this.tool = opts.tool;
81
+ this.approval = opts.approval;
82
+ this.now = opts.now ?? (() => Date.now());
83
+ }
84
+ /** True once RUN_FINISHED or RUN_ERROR has been seen; no retry after this. */
85
+ get finished() {
86
+ return this.terminal;
87
+ }
88
+ /** Interrupts carried by the terminal RUN_FINISHED, if any. */
89
+ get interrupts() {
90
+ return this.pendingInterrupts;
91
+ }
92
+ blocks() {
93
+ const content = [];
94
+ if (this.reasoning) content.push({
95
+ type: "thinking",
96
+ thinking: this.reasoning
97
+ });
98
+ if (this.text) content.push({
99
+ type: "text",
100
+ text: this.text
101
+ });
102
+ for (const block of this.a2ui) content.push({
103
+ type: "a2ui",
104
+ a2ui: block
105
+ });
106
+ return content;
107
+ }
108
+ emitDelta() {
109
+ const content = this.blocks();
110
+ if (content.length === 0) return;
111
+ this.chat({
112
+ runId: this.runId,
113
+ sessionKey: this.sessionKey,
114
+ seq: ++this.seq,
115
+ state: "delta",
116
+ message: {
117
+ role: "assistant",
118
+ content,
119
+ timestamp: this.now()
120
+ }
121
+ });
122
+ }
123
+ emitFinal() {
124
+ const content = this.blocks();
125
+ this.chat({
126
+ runId: this.runId,
127
+ sessionKey: this.sessionKey,
128
+ seq: ++this.seq,
129
+ state: "final",
130
+ ...content.length > 0 ? { message: {
131
+ role: "assistant",
132
+ content,
133
+ timestamp: this.now()
134
+ } } : {}
135
+ });
136
+ }
137
+ /** Surface a transport failure the way a run error is surfaced. */
138
+ fail(message) {
139
+ if (this.terminal) return;
140
+ this.terminal = true;
141
+ this.chat({
142
+ runId: this.runId,
143
+ sessionKey: this.sessionKey,
144
+ seq: ++this.seq,
145
+ state: "error",
146
+ errorMessage: message
147
+ });
148
+ }
149
+ handleEvent(event) {
150
+ switch (event.type) {
151
+ case "RUN_STARTED":
152
+ this.textMessageId = null;
153
+ this.text = "";
154
+ this.reasoning = "";
155
+ break;
156
+ case "TEXT_MESSAGE_START": {
157
+ const id = event.messageId ?? null;
158
+ this.textMessageId = id;
159
+ this.text = "";
160
+ if (id !== null) {
161
+ const before = this.reasoningAtStart.get(id);
162
+ if (before === void 0) this.reasoningAtStart.set(id, this.reasoning);
163
+ else this.reasoning = before;
164
+ }
165
+ break;
166
+ }
167
+ case "TEXT_MESSAGE_CONTENT":
168
+ if (event.messageId && event.messageId !== this.textMessageId) {
169
+ this.textMessageId = event.messageId;
170
+ this.text = "";
171
+ }
172
+ this.text += event.delta ?? "";
173
+ this.emitDelta();
174
+ break;
175
+ case "REASONING_MESSAGE_CONTENT":
176
+ this.reasoning += event.delta ?? "";
177
+ this.emitDelta();
178
+ break;
179
+ case "TOOL_CALL_START":
180
+ this.tool?.({
181
+ event: "tool.start",
182
+ data: {
183
+ ...event.toolCallId === void 0 ? {} : { id: event.toolCallId },
184
+ ...event.toolCallName === void 0 ? {} : { name: event.toolCallName },
185
+ startedAt: this.now()
186
+ }
187
+ });
188
+ break;
189
+ case "TOOL_CALL_ARGS":
190
+ this.accumulateArgs(event);
191
+ break;
192
+ case "TOOL_CALL_END":
193
+ this.flushArgs(event.toolCallId);
194
+ break;
195
+ case "TOOL_CALL_RESULT":
196
+ this.tool?.({
197
+ event: event.metadata?.isError === true ? "tool.error" : "tool.complete",
198
+ data: {
199
+ ...event.toolCallId === void 0 ? {} : { id: event.toolCallId },
200
+ output: event.content,
201
+ completedAt: this.now()
202
+ }
203
+ });
204
+ break;
205
+ case "CUSTOM":
206
+ this.handleCustom(event);
207
+ break;
208
+ case "RUN_FINISHED":
209
+ this.terminal = true;
210
+ if (event.outcome?.type === "interrupt") this.pendingInterrupts.push(...event.outcome.interrupts ?? []);
211
+ this.emitFinal();
212
+ break;
213
+ case "RUN_ERROR":
214
+ this.terminal = true;
215
+ this.chat({
216
+ runId: this.runId,
217
+ sessionKey: this.sessionKey,
218
+ seq: ++this.seq,
219
+ state: "error",
220
+ errorMessage: event.message ?? "An error occurred"
221
+ });
222
+ }
223
+ }
224
+ /**
225
+ * Accumulate one `TOOL_CALL_ARGS` delta and publish the best reading of it.
226
+ *
227
+ * Published on every delta rather than only at `TOOL_CALL_END`, because a
228
+ * run that is interrupted between the two — an approval, an abort, a dropped
229
+ * stream — would otherwise show a tool card with no input at all, which is
230
+ * the state the card exists to avoid.
231
+ */
232
+ accumulateArgs(event) {
233
+ const id = event.toolCallId;
234
+ if (!id) return;
235
+ const next = (this.args.get(id) ?? "") + (event.delta ?? "");
236
+ this.args.set(id, next);
237
+ this.publishArgs(id, next);
238
+ }
239
+ flushArgs(id) {
240
+ if (!id) return;
241
+ const raw = this.args.get(id);
242
+ if (raw === void 0) return;
243
+ this.args.delete(id);
244
+ this.publishArgs(id, raw);
245
+ }
246
+ publishArgs(id, raw) {
247
+ if (!raw) return;
248
+ this.tool?.({
249
+ event: "tool.args",
250
+ data: {
251
+ id,
252
+ input: parseArgs(raw)
253
+ }
254
+ });
255
+ }
256
+ /**
257
+ * The block with any message this surface has already been sent removed;
258
+ * `null` when nothing in it is new.
259
+ */
260
+ freshA2UI(block) {
261
+ if (!block) return null;
262
+ let seen = this.seenA2ui.get(block.surfaceId);
263
+ if (!seen) {
264
+ seen = /* @__PURE__ */ new Set();
265
+ this.seenA2ui.set(block.surfaceId, seen);
266
+ }
267
+ const messages = [];
268
+ for (const message of block.messages) {
269
+ const key = stableKey(message);
270
+ if (seen.has(key)) continue;
271
+ seen.add(key);
272
+ messages.push(message);
273
+ }
274
+ if (messages.length === 0 && !block.error) return null;
275
+ return {
276
+ ...block,
277
+ messages
278
+ };
279
+ }
280
+ handleCustom(event) {
281
+ const value = event.value ?? {};
282
+ const id = typeof value.id === "string" ? value.id : "";
283
+ switch (event.name) {
284
+ case AGUI_CUSTOM_NAMES.TOOL_PROGRESS:
285
+ this.tool?.({
286
+ event: "tool.progress",
287
+ data: {
288
+ ...typeof value.toolCallId === "string" ? { id: value.toolCallId } : {},
289
+ output: value.partialResult
290
+ }
291
+ });
292
+ break;
293
+ case AGUI_CUSTOM_NAMES.APPROVAL_REQUEST: {
294
+ const request = value.request ?? {};
295
+ this.approval?.({
296
+ type: "request",
297
+ request: {
298
+ id,
299
+ toolName: typeof request.command === "string" ? request.command : "exec",
300
+ args: request,
301
+ timestamp: typeof value.createdAtMs === "number" ? value.createdAtMs : this.now(),
302
+ runId: this.runId,
303
+ sessionKey: this.sessionKey
304
+ }
305
+ });
306
+ break;
307
+ }
308
+ case AGUI_CUSTOM_NAMES.A2UI: {
309
+ const block = this.freshA2UI(readA2UIBlock(value));
310
+ if (block) {
311
+ this.a2ui = foldA2UIBlocks([...this.a2ui, block]);
312
+ this.emitDelta();
313
+ }
314
+ break;
315
+ }
316
+ case AGUI_CUSTOM_NAMES.APPROVAL_RESOLVED: this.approval?.({
317
+ type: "resolved",
318
+ id,
319
+ decision: typeof value.decision === "string" && DECISIONS.includes(value.decision) ? value.decision : null
320
+ });
321
+ }
322
+ }
323
+ };
324
+ /**
325
+ * A message's identity for de-duplication.
326
+ *
327
+ * `JSON.stringify` is enough: these are payloads the transport just parsed out
328
+ * of JSON, so key order is the order the server wrote, and the same message
329
+ * arriving twice on a re-attach serialises identically both times.
330
+ */
331
+ function stableKey(message) {
332
+ try {
333
+ return JSON.stringify(message) ?? "undefined";
334
+ } catch {
335
+ return `${Math.random()}`;
336
+ }
337
+ }
338
+ /** Arguments as an object when the accumulation parses, as the raw text otherwise. */
339
+ function parseArgs(raw) {
340
+ try {
341
+ return JSON.parse(raw);
342
+ } catch {
343
+ return raw;
344
+ }
345
+ }
346
+ //#endregion
347
+ export { AGUI_CUSTOM_NAMES, AgentChatBridge };
348
+
349
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","names":[],"sources":["../../src/agent-chat/bridge.ts"],"sourcesContent":["/**\n * AG-UI events -> the three `AgentChatClient` sinks.\n *\n * Ported from `cortena-shared/src/agui/bridge.ts` (Cortena monorepo, DESIGN-37\n * step 1). `cortena-shared` is not published, so the translation lives here;\n * keep the two in step. It knows nothing about HTTP or `@ag-ui/client`, which\n * is why the tests can drive it with a literal array of events.\n *\n * One change against the original, and it is the reason this file is not a\n * copy: **TOOL_CALL_ARGS is handled.** cortenacore's mapper emits\n * `TOOL_CALL_START` -> `TOOL_CALL_ARGS { toolCallId, delta }` -> `TOOL_CALL_END`\n * at tool-start time, with the whole argument object stringified into one\n * `delta`. The original bridge had no case for it, so every tool card in the\n * AG-UI path showed a name and a spinner and never showed what the tool was\n * called with. The deltas are accumulated per call id and parsed when they\n * form valid JSON, which is also correct for a transport that streams the\n * arguments a fragment at a time.\n */\n\nimport { type A2UIChatBlock, foldA2UIBlocks, readA2UIBlock } from \"./a2ui-block\";\nimport type {\n AgentApprovalEvent,\n AgentChatEvent,\n AgentToolEvent,\n AgentApprovalDecision,\n} from \"./types\";\n\n/** The subset of an AG-UI event this bridge reads. */\nexport interface AguiIncomingEvent {\n type: string;\n messageId?: string;\n delta?: string;\n toolCallId?: string;\n toolCallName?: string;\n content?: string;\n message?: string;\n name?: string;\n value?: unknown;\n metadata?: Record<string, unknown>;\n snapshot?: unknown;\n outcome?: { type?: string; interrupts?: Array<Record<string, unknown>> };\n}\n\n/** `CUSTOM` event names cortenacore emits that this bridge acts on. */\nexport const AGUI_CUSTOM_NAMES = {\n CHAT_ABORTED: \"cortena.chat.aborted\",\n TOOL_PROGRESS: \"cortena.tool.progress\",\n APPROVAL_REQUEST: \"cortena.approval.request\",\n APPROVAL_RESOLVED: \"cortena.approval.resolved\",\n A2UI: \"cortena.a2ui\",\n} as const;\n\nexport interface AgentChatBridgeOptions {\n runId: string;\n sessionKey: string;\n chat: (event: AgentChatEvent) => void;\n tool?: (event: AgentToolEvent) => void;\n approval?: (event: AgentApprovalEvent) => void;\n now?: () => number;\n}\n\ntype ContentBlock = {\n type: \"thinking\" | \"text\" | \"a2ui\";\n thinking?: string;\n text?: string;\n a2ui?: A2UIChatBlock;\n};\n\nconst DECISIONS: readonly string[] = [\"allow-once\", \"allow-always\", \"deny\"];\n\nexport class AgentChatBridge {\n readonly runId: string;\n readonly sessionKey: string;\n\n private readonly chat: (event: AgentChatEvent) => void;\n private readonly tool: ((event: AgentToolEvent) => void) | undefined;\n private readonly approval: ((event: AgentApprovalEvent) => void) | undefined;\n private readonly now: () => number;\n\n private seq = 0;\n /** Text of the assistant message currently streaming. */\n private text = \"\";\n private textMessageId: string | null = null;\n private reasoning = \"\";\n /**\n * The reasoning accumulated before each assistant message began, by message\n * id, and the whole of the re-attach defence for the chain of thought.\n *\n * A re-attach re-streams the run from the top, so every REASONING delta\n * arrives a second time. Restoring this snapshot when a message id starts\n * again rewinds the buffer to exactly the point the first pass reached,\n * whichever side of `TEXT_MESSAGE_START` the reasoning arrived on.\n */\n private readonly reasoningAtStart = new Map<string, string>();\n /**\n * A2UI blocks seen this run. The chat treats the content array as a\n * snapshot, so every emission carries all of them — the same rule the text\n * follows, and what makes a re-attach land in the same place.\n */\n private a2ui: A2UIChatBlock[] = [];\n /** Accumulated `TOOL_CALL_ARGS` deltas, per tool call id. */\n private readonly args = new Map<string, string>();\n /**\n * A2UI messages already folded onto each surface, by their serialised form.\n *\n * A re-attach re-streams the run from the beginning, so every\n * `CUSTOM cortena.a2ui` arrives again. Folding concatenates, so without this\n * the surface's message list doubled on the first reconnection and trebled\n * on the second — a table drawn once, then drawn again underneath itself.\n */\n private readonly seenA2ui = new Map<string, Set<string>>();\n private terminal = false;\n private readonly pendingInterrupts: Array<Record<string, unknown>> = [];\n\n constructor(opts: AgentChatBridgeOptions) {\n this.runId = opts.runId;\n this.sessionKey = opts.sessionKey;\n this.chat = opts.chat;\n this.tool = opts.tool;\n this.approval = opts.approval;\n this.now = opts.now ?? (() => Date.now());\n }\n\n /** True once RUN_FINISHED or RUN_ERROR has been seen; no retry after this. */\n get finished(): boolean {\n return this.terminal;\n }\n\n /** Interrupts carried by the terminal RUN_FINISHED, if any. */\n get interrupts(): ReadonlyArray<Record<string, unknown>> {\n return this.pendingInterrupts;\n }\n\n private blocks(): ContentBlock[] {\n const content: ContentBlock[] = [];\n if (this.reasoning) {\n content.push({ type: \"thinking\", thinking: this.reasoning });\n }\n if (this.text) {\n content.push({ type: \"text\", text: this.text });\n }\n for (const block of this.a2ui) {\n content.push({ type: \"a2ui\", a2ui: block });\n }\n return content;\n }\n\n private emitDelta(): void {\n const content = this.blocks();\n if (content.length === 0) {\n return;\n }\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"delta\",\n message: { role: \"assistant\", content, timestamp: this.now() },\n });\n }\n\n private emitFinal(): void {\n const content = this.blocks();\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"final\",\n ...(content.length > 0\n ? { message: { role: \"assistant\", content, timestamp: this.now() } }\n : {}),\n });\n }\n\n /** Surface a transport failure the way a run error is surfaced. */\n fail(message: string): void {\n if (this.terminal) {\n return;\n }\n this.terminal = true;\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"error\",\n errorMessage: message,\n });\n }\n\n handleEvent(event: AguiIncomingEvent): void {\n switch (event.type) {\n case \"RUN_STARTED\":\n // The run begins here, so the streaming buffers do too. This is the\n // only place the chain of thought is cleared outright.\n this.textMessageId = null;\n this.text = \"\";\n this.reasoning = \"\";\n break;\n case \"TEXT_MESSAGE_START\": {\n // A new assistant message replaces the text buffer, and *only* the\n // text buffer. cortenacore emits the REASONING deltas before the\n // message they explain, so clearing the reasoning here threw away the\n // chain of thought a moment after it arrived — the thinking block\n // never rendered at all. Re-attach doubling is handled instead by\n // rewinding to the snapshot taken when this message id first started.\n const id = event.messageId ?? null;\n this.textMessageId = id;\n this.text = \"\";\n if (id !== null) {\n const before = this.reasoningAtStart.get(id);\n if (before === undefined) {\n this.reasoningAtStart.set(id, this.reasoning);\n } else {\n this.reasoning = before;\n }\n }\n break;\n }\n case \"TEXT_MESSAGE_CONTENT\":\n if (event.messageId && event.messageId !== this.textMessageId) {\n this.textMessageId = event.messageId;\n this.text = \"\";\n }\n this.text += event.delta ?? \"\";\n this.emitDelta();\n break;\n case \"REASONING_MESSAGE_CONTENT\":\n this.reasoning += event.delta ?? \"\";\n this.emitDelta();\n break;\n case \"TOOL_CALL_START\":\n this.tool?.({\n event: \"tool.start\",\n data: {\n ...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),\n ...(event.toolCallName === undefined ? {} : { name: event.toolCallName }),\n startedAt: this.now(),\n },\n });\n break;\n case \"TOOL_CALL_ARGS\":\n this.accumulateArgs(event);\n break;\n case \"TOOL_CALL_END\":\n // Nothing more will arrive for this call's arguments; emit whatever\n // accumulated, even when it never parsed, so the card shows something.\n this.flushArgs(event.toolCallId);\n break;\n case \"TOOL_CALL_RESULT\":\n this.tool?.({\n event: event.metadata?.isError === true ? \"tool.error\" : \"tool.complete\",\n data: {\n ...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),\n output: event.content,\n completedAt: this.now(),\n },\n });\n break;\n case \"CUSTOM\":\n this.handleCustom(event);\n break;\n case \"RUN_FINISHED\":\n this.terminal = true;\n if (event.outcome?.type === \"interrupt\") {\n this.pendingInterrupts.push(...(event.outcome.interrupts ?? []));\n }\n this.emitFinal();\n break;\n case \"RUN_ERROR\":\n this.terminal = true;\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"error\",\n errorMessage: event.message ?? \"An error occurred\",\n });\n break;\n default:\n break;\n }\n }\n\n /**\n * Accumulate one `TOOL_CALL_ARGS` delta and publish the best reading of it.\n *\n * Published on every delta rather than only at `TOOL_CALL_END`, because a\n * run that is interrupted between the two — an approval, an abort, a dropped\n * stream — would otherwise show a tool card with no input at all, which is\n * the state the card exists to avoid.\n */\n private accumulateArgs(event: AguiIncomingEvent): void {\n const id = event.toolCallId;\n if (!id) {\n return;\n }\n const next = (this.args.get(id) ?? \"\") + (event.delta ?? \"\");\n this.args.set(id, next);\n this.publishArgs(id, next);\n }\n\n private flushArgs(id?: string): void {\n if (!id) {\n return;\n }\n const raw = this.args.get(id);\n if (raw === undefined) {\n return;\n }\n this.args.delete(id);\n this.publishArgs(id, raw);\n }\n\n private publishArgs(id: string, raw: string): void {\n if (!raw) {\n return;\n }\n this.tool?.({ event: \"tool.args\", data: { id, input: parseArgs(raw) } });\n }\n\n /**\n * The block with any message this surface has already been sent removed;\n * `null` when nothing in it is new.\n */\n private freshA2UI(block: A2UIChatBlock | null): A2UIChatBlock | null {\n if (!block) {\n return null;\n }\n let seen = this.seenA2ui.get(block.surfaceId);\n if (!seen) {\n seen = new Set();\n this.seenA2ui.set(block.surfaceId, seen);\n }\n const messages: unknown[] = [];\n for (const message of block.messages) {\n const key = stableKey(message);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n messages.push(message);\n }\n // An error still deserves a card even when its messages are all repeats.\n if (messages.length === 0 && !block.error) {\n return null;\n }\n return { ...block, messages };\n }\n\n private handleCustom(event: AguiIncomingEvent): void {\n const value = (event.value ?? {}) as Record<string, unknown>;\n const id = typeof value.id === \"string\" ? value.id : \"\";\n switch (event.name) {\n case AGUI_CUSTOM_NAMES.TOOL_PROGRESS:\n this.tool?.({\n event: \"tool.progress\",\n data: {\n ...(typeof value.toolCallId === \"string\" ? { id: value.toolCallId } : {}),\n output: value.partialResult,\n },\n });\n break;\n case AGUI_CUSTOM_NAMES.APPROVAL_REQUEST: {\n const request = (value.request ?? {}) as Record<string, unknown>;\n this.approval?.({\n type: \"request\",\n request: {\n id,\n toolName: typeof request.command === \"string\" ? request.command : \"exec\",\n args: request,\n timestamp: typeof value.createdAtMs === \"number\" ? value.createdAtMs : this.now(),\n // Captured here, where the run and the session are facts about the\n // event rather than about whatever is in flight when the user\n // eventually presses a button.\n runId: this.runId,\n sessionKey: this.sessionKey,\n },\n });\n break;\n }\n case AGUI_CUSTOM_NAMES.A2UI: {\n // One event per block; folding here means two pushes to one surface\n // reach the renderer as a single message list, which is how a surface\n // the agent streams in pieces ends up drawn once.\n const block = this.freshA2UI(readA2UIBlock(value));\n if (block) {\n this.a2ui = foldA2UIBlocks([...this.a2ui, block]);\n this.emitDelta();\n }\n break;\n }\n case AGUI_CUSTOM_NAMES.APPROVAL_RESOLVED:\n this.approval?.({\n type: \"resolved\",\n id,\n decision:\n typeof value.decision === \"string\" && DECISIONS.includes(value.decision)\n ? (value.decision as AgentApprovalDecision)\n : null,\n });\n break;\n default:\n // cortena.chat.aborted needs no change here: abort() has already reset\n // the local streaming state before the request went out.\n break;\n }\n }\n}\n\n/**\n * A message's identity for de-duplication.\n *\n * `JSON.stringify` is enough: these are payloads the transport just parsed out\n * of JSON, so key order is the order the server wrote, and the same message\n * arriving twice on a re-attach serialises identically both times.\n */\nfunction stableKey(message: unknown): string {\n try {\n return JSON.stringify(message) ?? \"undefined\";\n } catch {\n // Circular, which a JSON transport cannot produce. Never equal to anything.\n return `${Math.random()}`;\n }\n}\n\n/** Arguments as an object when the accumulation parses, as the raw text otherwise. */\nfunction parseArgs(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { AgentChatMessage } from "./types.js";
3
+ //#region src/agent-chat/session.d.ts
4
+ /**
5
+ * A new session key, minted client-side.
6
+ *
7
+ * `agent:<agentId>:new-<Date.now()>-<random36>`. cortenacore reads the
8
+ * `agent:<id>:` prefix to bind the session to that agent's template, and the
9
+ * key is the AG-UI `threadId` verbatim, prefix included. Minting it rather than
10
+ * asking for one is what lets the first message go out immediately.
11
+ */
12
+ export declare function mintSessionKey(agentId: string): string;
13
+ /** Was this key minted locally for a chat that had no session yet? */
14
+ export declare function isProvisionalSessionKey(key: string): boolean;
15
+ /** Does this session belong to this agent? `sessions.list` is filtered by it. */
16
+ export declare function isSessionOfAgent(key: string, agentId: string): boolean;
17
+ /** The `sessionStorage` name the current key is kept under, per extension. */
18
+ export declare function sessionStorageKey(scope: string): string;
19
+ /** The `sessionStorage` name the open/collapsed state is kept under. */
20
+ export declare function viewStorageKey(scope: string): string;
21
+ export declare function readStoredSessionKey(scope: string): string | null;
22
+ export declare function writeStoredSessionKey(scope: string, key: string | null): void;
23
+ /** Raw transcript records to the messages the list renders. */
24
+ export declare function parseHistoryMessages(raw: readonly unknown[]): AgentChatMessage[];
25
+ /**
26
+ * The history shrink guard.
27
+ *
28
+ * A history load must not shrink the session already on screen. The first
29
+ * message of a new chat loads history while the turn is still in flight and the
30
+ * user's message has not been persisted yet; replacing wholesale threw that
31
+ * message away, and the reply survived, so the symptom was a conversation with
32
+ * the answer but not the question.
33
+ *
34
+ * Only the same session, and only against losing messages: a longer or equal
35
+ * history still replaces, so an edit, a deletion made elsewhere and a compaction
36
+ * all land normally.
37
+ */
38
+ export declare function wouldShrinkHistory(params: {
39
+ currentSessionKey: string | null;
40
+ loadedSessionKey: string;
41
+ currentCount: number;
42
+ loadedCount: number;
43
+ }): boolean;
44
+ /**
45
+ * Should this message be hidden?
46
+ *
47
+ * The same chain cortenaweb's bubble applies, in the same order. It exists
48
+ * because tool plumbing leaks into a transcript in several recognisable shapes,
49
+ * and a chat that renders them reads as broken. Tool detail is not lost: it is
50
+ * in the tool strip and in the chain of thought.
51
+ *
52
+ * A reply that is only UI the agent drew has no text at all, so an A2UI block
53
+ * counts as content — without that, the surface the user asked for is filtered
54
+ * away and nothing appears.
55
+ */
56
+ export declare function isHiddenMessage(message: AgentChatMessage): boolean;
57
+ //#endregion
58
+ //# sourceMappingURL=session.d.ts.map