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.
- package/LICENSE +7 -0
- package/README.md +149 -1
- package/dist/a2ui/views.js +2 -2
- package/dist/agent-chat/a2ui-block.d.ts +60 -0
- package/dist/agent-chat/a2ui-block.js +69 -0
- package/dist/agent-chat/a2ui-block.js.map +1 -0
- package/dist/agent-chat/agui-client.d.ts +40 -0
- package/dist/agent-chat/agui-client.js +251 -0
- package/dist/agent-chat/agui-client.js.map +1 -0
- package/dist/agent-chat/bridge.d.ts +109 -0
- package/dist/agent-chat/bridge.js +349 -0
- package/dist/agent-chat/bridge.js.map +1 -0
- package/dist/agent-chat/session.d.ts +58 -0
- package/dist/agent-chat/session.js +249 -0
- package/dist/agent-chat/session.js.map +1 -0
- package/dist/agent-chat/store.d.ts +67 -0
- package/dist/agent-chat/store.js +548 -0
- package/dist/agent-chat/store.js.map +1 -0
- package/dist/agent-chat/types.d.ts +187 -0
- package/dist/agent-chat/types.js +17 -0
- package/dist/agent-chat/types.js.map +1 -0
- package/dist/agent-chat.d.ts +10 -0
- package/dist/agent-chat.js +10 -0
- package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
- package/dist/components/admin-permissions/admin-permissions.js +101 -0
- package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
- package/dist/components/admin-permissions/context.d.ts +70 -0
- package/dist/components/admin-permissions/context.js +258 -0
- package/dist/components/admin-permissions/context.js.map +1 -0
- package/dist/components/admin-permissions/index.d.ts +10 -0
- package/dist/components/admin-permissions/licence.d.ts +15 -0
- package/dist/components/admin-permissions/licence.js +78 -0
- package/dist/components/admin-permissions/licence.js.map +1 -0
- package/dist/components/admin-permissions/matrix.d.ts +20 -0
- package/dist/components/admin-permissions/matrix.js +191 -0
- package/dist/components/admin-permissions/matrix.js.map +1 -0
- package/dist/components/admin-permissions/members.d.ts +18 -0
- package/dist/components/admin-permissions/members.js +185 -0
- package/dist/components/admin-permissions/members.js.map +1 -0
- package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
- package/dist/components/admin-permissions/role-assignment.js +174 -0
- package/dist/components/admin-permissions/role-assignment.js.map +1 -0
- package/dist/components/admin-permissions/roles.d.ts +25 -0
- package/dist/components/admin-permissions/roles.js +168 -0
- package/dist/components/admin-permissions/roles.js.map +1 -0
- package/dist/components/admin-permissions/types.d.ts +152 -0
- package/dist/components/admin-permissions/types.js +63 -0
- package/dist/components/admin-permissions/types.js.map +1 -0
- package/dist/components/agent-chat-popup.d.ts +29 -0
- package/dist/components/agent-chat-popup.js +188 -0
- package/dist/components/agent-chat-popup.js.map +1 -0
- package/dist/components/agent-chat.d.ts +143 -0
- package/dist/components/agent-chat.js +578 -0
- package/dist/components/agent-chat.js.map +1 -0
- package/dist/components/app-shell.d.ts +126 -0
- package/dist/components/app-shell.js +297 -0
- package/dist/components/app-shell.js.map +1 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button-link.js +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/checkbox.d.ts +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/consent-screen.d.ts +65 -0
- package/dist/components/consent-screen.js +123 -0
- package/dist/components/consent-screen.js.map +1 -0
- package/dist/components/data-table/data-table.d.ts +15 -1
- package/dist/components/data-table/data-table.js +18 -4
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/data-table/index.d.ts +4 -4
- package/dist/components/data-table/parts.d.ts +27 -3
- package/dist/components/data-table/parts.js +175 -55
- package/dist/components/data-table/parts.js.map +1 -1
- package/dist/components/data-table/types.d.ts +61 -0
- package/dist/components/data-table/use-data-table.js +91 -6
- package/dist/components/data-table/use-data-table.js.map +1 -1
- package/dist/components/data-table/use-server-source.js +119 -28
- package/dist/components/data-table/use-server-source.js.map +1 -1
- package/dist/components/help-panel.d.ts +131 -0
- package/dist/components/help-panel.js +545 -0
- package/dist/components/help-panel.js.map +1 -0
- package/dist/components/login-screen.d.ts +127 -0
- package/dist/components/login-screen.js +339 -0
- package/dist/components/login-screen.js.map +1 -0
- package/dist/components/session-guard.d.ts +268 -0
- package/dist/components/session-guard.js +632 -0
- package/dist/components/session-guard.js.map +1 -0
- package/dist/components/toast.d.ts +1 -1
- package/dist/core.d.ts +5 -1
- package/dist/core.js +11 -7
- package/dist/data-table.d.ts +13 -4
- package/dist/data-table.js +10 -2
- package/dist/hooks/use-cortena-theme.js +49 -3
- package/dist/hooks/use-cortena-theme.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +21 -8
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.js +2 -1
- package/package.json +16 -4
- package/src/agent-chat/a2ui-block.ts +118 -0
- package/src/agent-chat/agui-client.ts +405 -0
- package/src/agent-chat/bridge.ts +433 -0
- package/src/agent-chat/session.ts +392 -0
- package/src/agent-chat/store.ts +738 -0
- package/src/agent-chat/types.ts +213 -0
- package/src/components/admin-permissions/admin-permissions.tsx +130 -0
- package/src/components/admin-permissions/context.tsx +376 -0
- package/src/components/admin-permissions/index.tsx +32 -0
- package/src/components/admin-permissions/licence.tsx +84 -0
- package/src/components/admin-permissions/matrix.tsx +257 -0
- package/src/components/admin-permissions/members.tsx +204 -0
- package/src/components/admin-permissions/role-assignment.tsx +239 -0
- package/src/components/admin-permissions/roles.tsx +169 -0
- package/src/components/admin-permissions/types.ts +231 -0
- package/src/components/agent-chat-popup.tsx +289 -0
- package/src/components/agent-chat.tsx +843 -0
- package/src/components/app-shell.tsx +502 -0
- package/src/components/consent-screen.tsx +239 -0
- package/src/components/data-table/data-table.tsx +36 -0
- package/src/components/data-table/index.tsx +6 -1
- package/src/components/data-table/parts.tsx +223 -47
- package/src/components/data-table/types.ts +68 -0
- package/src/components/data-table/use-data-table.ts +152 -4
- package/src/components/data-table/use-server-source.ts +150 -12
- package/src/components/help-panel.tsx +765 -0
- package/src/components/login-screen.tsx +479 -0
- package/src/components/session-guard.tsx +1071 -0
- package/src/entries/agent-chat.ts +113 -0
- package/src/entries/core.ts +8 -0
- package/src/entries/data-table.ts +41 -0
- package/src/entries/markdown.ts +25 -0
- package/src/hooks/use-cortena-theme.ts +63 -4
- package/src/index.ts +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 Ascendence AI Technology Pvt Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This package is published for use with the Cortena platform by licensed
|
|
4
|
+
Cortena customers and partners only. No permission is granted to copy,
|
|
5
|
+
modify, redistribute or use it outside a Cortena deployment.
|
|
6
|
+
|
|
7
|
+
Provided as is, without warranty.
|
package/README.md
CHANGED
|
@@ -28,18 +28,59 @@ consumer's bundler getting tree-shaking right:
|
|
|
28
28
|
|
|
29
29
|
| entry | what it holds | what it costs |
|
|
30
30
|
| --- | --- | --- |
|
|
31
|
-
| `cortena-ui/core` | primitives, overlays, light form controls, `cn`, `useCortenaTheme` | nothing heavy |
|
|
31
|
+
| `cortena-ui/core` | primitives, overlays, light form controls, `AppShell`, `cn`, `useCortenaTheme` | nothing heavy |
|
|
32
32
|
| `cortena-ui/form` | `Form`, `useForm`, `zodResolver`, date pickers, `Dropzone` | react-hook-form, zod, react-day-picker, react-dropzone |
|
|
33
33
|
| `cortena-ui/data-table` | `DataTable` and its parts | TanStack Table and Virtual; exceljs on the first export |
|
|
34
34
|
| `cortena-ui/chart` | `Chart`, themed Recharts primitives | an engine on the first chart drawn, not on import |
|
|
35
35
|
| `cortena-ui/markdown` | `Markdown` | react-markdown, remark-gfm, rehype-sanitize |
|
|
36
36
|
| `cortena-ui/a2ui` | the A2UI catalogue and renderer | most of the package, by design |
|
|
37
37
|
| `cortena-ui/sortable-list` | `SortableList`, `SortableHandle`, `arrayMove` | dnd-kit |
|
|
38
|
+
| `cortena-ui/agent-chat` | `AgentChatPopup`, `AgentChat`, `createAguiAgentChatClient` | the A2UI catalogue, plus `@ag-ui/client` (rxjs, zod 3, uuid, protobuf) |
|
|
38
39
|
|
|
39
40
|
Each component has exactly one home, so the barrel re-exports every entry
|
|
40
41
|
without an ambiguous name. **Adding a component means adding it to its area
|
|
41
42
|
entry in `src/entries/`, not to `src/index.ts`.**
|
|
42
43
|
|
|
44
|
+
`AgentChat` has its own entry for the same kind of reason: `@ag-ui/client` is
|
|
45
|
+
the run transport, and an extension that draws a button should not download a
|
|
46
|
+
chat stream it never opens. It is also **not re-exported from the root barrel**,
|
|
47
|
+
so `import { Button } from "cortena-ui"` cannot reach it however the consumer's
|
|
48
|
+
bundler behaves. `pnpm bundle:probe` measures it, and
|
|
49
|
+
`test/bundle-probe.test.mjs` fails if `@ag-ui/*` turns up in any other probe.
|
|
50
|
+
|
|
51
|
+
`@ag-ui/client` is an **optional peer**, not a dependency: an extension that
|
|
52
|
+
uses this entry installs it itself, at a version its own package.json names.
|
|
53
|
+
|
|
54
|
+
```jsonc
|
|
55
|
+
"peerDependencies": { "@ag-ui/client": "^0.0.59" },
|
|
56
|
+
"peerDependenciesMeta": { "@ag-ui/client": { "optional": true } }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
As a dependency it was pinned to one exact build for the whole fleet, and an
|
|
60
|
+
app that also talks AG-UI directly — which is every app with a second agent
|
|
61
|
+
surface — resolved two copies. Two copies of an rxjs-based client is two event
|
|
62
|
+
pipelines and two zod registries, and the symptom is a subscription that never
|
|
63
|
+
fires. Optional, because only `createAguiAgentChatClient` imports it: `AgentChat`,
|
|
64
|
+
`AgentChatPopup` and `useAgentChat` are written against the `AgentChatClient`
|
|
65
|
+
interface, so an extension on another transport installs nothing.
|
|
66
|
+
|
|
67
|
+
And it is loaded **lazily**, inside `createAguiAgentChatClient`, by dynamic
|
|
68
|
+
import. A static import meant the optional peer was not optional at all: with
|
|
69
|
+
it uninstalled the whole `cortena-ui/agent-chat` module failed to evaluate, so
|
|
70
|
+
an extension on another transport could not draw `AgentChatPopup` either — a
|
|
71
|
+
package it had never heard of decided whether its chat rendered, and said so in
|
|
72
|
+
a resolution error. The entry now loads without the peer, and only a run needs
|
|
73
|
+
it; when it is absent the chat shows
|
|
74
|
+
|
|
75
|
+
> cortena-ui/agent-chat: the AG-UI transport needs the optional peer
|
|
76
|
+
> "@ag-ui/client". Install it (pnpm add @ag-ui/client), or pass your own
|
|
77
|
+
> AgentChatClient to AgentChat.
|
|
78
|
+
|
|
79
|
+
The specifier stays a literal, so a bundler still splits the transport into a
|
|
80
|
+
chunk of its own and `test/bundle-probe.test.mjs` still measures that the
|
|
81
|
+
agent-chat entry carries it. `test/agent-chat-peer.test.mjs` runs the built
|
|
82
|
+
entry in a child process with the peer made unresolvable.
|
|
83
|
+
|
|
43
84
|
`SortableList` has its own entry for a specific reason rather than a stylistic
|
|
44
85
|
one: `@dnd-kit/core` and `@dnd-kit/sortable` declare no `"sideEffects"` in their
|
|
45
86
|
package.json, so a bundler must assume importing them does something observable
|
|
@@ -86,6 +127,113 @@ regression; `test/chart-bundle.test.tsx` says which line caused it.
|
|
|
86
127
|
check in light, dark and system-dark; add an entry with every variant and
|
|
87
128
|
size when adding a component.
|
|
88
129
|
|
|
130
|
+
## Shell chrome
|
|
131
|
+
|
|
132
|
+
`AppShell` is the chrome every extension wears (§9 of
|
|
133
|
+
how-to-create-a-cortena-extension, audit rule P-10): the registered brand mark
|
|
134
|
+
and the extension name top left, the avatar menu — the only settings entry
|
|
135
|
+
point — top right, and one fixed bottom-right cluster holding the theme toggle
|
|
136
|
+
then the help button. `BrandMark`, `BottomRightCluster`, `ThemeToggle`,
|
|
137
|
+
`HelpButton` and `documentTitle` are exported alongside it.
|
|
138
|
+
|
|
139
|
+
The mark comes from `cortena-design/marks` by `extension.id`, which is the same
|
|
140
|
+
file the Apps tile, the MCP app card, the agent pop-up pill and the favicon
|
|
141
|
+
draw. Adding a mark, and generating the three favicon files from it with
|
|
142
|
+
`cortena-design-favicon`, are both in `../../CONSUMING.md`, "Shell chrome and
|
|
143
|
+
favicon".
|
|
144
|
+
|
|
145
|
+
`helpPanel` is a slot until `HelpPanel` lands (EXTBP-24); `help.source` is the
|
|
146
|
+
functional document the panel will read.
|
|
147
|
+
|
|
148
|
+
## The agent pop-up
|
|
149
|
+
|
|
150
|
+
`AgentChatPopup` is the extension's own Cortena Agent, in the corner of every
|
|
151
|
+
screen (§17 of how-to-create-a-cortena-extension, audit rule P-25). It mounts in
|
|
152
|
+
`AppShell`'s `agentSlot`, so it stacks above the theme/help cluster rather than
|
|
153
|
+
beside it.
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { AppShell } from "cortena-ui";
|
|
157
|
+
import { AgentChatPopup, createAguiAgentChatClient } from "cortena-ui/agent-chat";
|
|
158
|
+
|
|
159
|
+
const extension = { id: "tasks", name: "Tasks" };
|
|
160
|
+
|
|
161
|
+
const client = createAguiAgentChatClient({
|
|
162
|
+
baseUrl: "/api/agent", // this extension's own gateway, not cortenacore
|
|
163
|
+
getToken: () => jwt, // the signed-in USER's JWT, never a service token
|
|
164
|
+
agentId: "tasks", // = the AgentTemplate slug = the extension id
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
<AppShell
|
|
168
|
+
extension={extension}
|
|
169
|
+
user={user}
|
|
170
|
+
agentSlot={<AgentChatPopup extension={extension} client={client} />}
|
|
171
|
+
/>;
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
A collapsed pill opens to a panel (420 x 640) and the panel header expands it to
|
|
175
|
+
full screen. **Cmd/Ctrl+J** toggles the pill and the panel; **Escape** steps full
|
|
176
|
+
screen -> panel -> pill, except while a run is streaming, where the composer
|
|
177
|
+
takes Escape to mean "stop the run". The chat is hidden rather than unmounted
|
|
178
|
+
while collapsed, so a run that started before the user collapsed it keeps
|
|
179
|
+
streaming and the pill shows an unread dot.
|
|
180
|
+
|
|
181
|
+
**Sessions are per window.** The current session key lives in `sessionStorage`
|
|
182
|
+
under a name scoped to the extension id, not in `localStorage`: opening the
|
|
183
|
+
extension in a second window starts a second session rather than adopting the
|
|
184
|
+
first window's. Every session stays listed and is resumable from any window, and
|
|
185
|
+
two windows on one session may both send — cortenacore serialises the runs. A
|
|
186
|
+
new key is minted client-side as `agent:<id>:new-<Date.now()>-<random36>` and
|
|
187
|
+
used immediately, so an in-flight first message is never lost.
|
|
188
|
+
|
|
189
|
+
### What the extension must proxy
|
|
190
|
+
|
|
191
|
+
The pop-up talks to one origin, `baseUrl`, and every call carries the user's
|
|
192
|
+
JWT. An extension's gateway proxies these to cortenacore; nothing here holds a
|
|
193
|
+
service credential.
|
|
194
|
+
|
|
195
|
+
| method + path | what it is |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `POST {base}/agui/run` | the run stream. AG-UI `RunAgentInput` in, `text/event-stream` out. `threadId` is the session key verbatim, prefix included |
|
|
198
|
+
| `POST {base}/agui/abort` | `{ runId, threadId }` |
|
|
199
|
+
| `POST {base}/agui/approval` | `{ id, decision, threadId }` — `allow-once`, `allow-always`, `deny`. A separate endpoint on purpose: a decision sent as state on a second `/agui/run` opens a second concurrent stream |
|
|
200
|
+
| `GET {base}/api/core/sessions/list` | the session drawer, filtered client-side to `agent:<id>:` |
|
|
201
|
+
| `GET {base}/api/core/chat/history` | `?sessionKey=&limit=100&offset=` |
|
|
202
|
+
| `POST {base}/tools/invoke` | only for an MCP App frame the host wires in — the app's own `tools/call`, scoped to its `mcp__<server>__` prefix |
|
|
203
|
+
| `GET /api/design-tokens` | only for an MCP App frame: the token stylesheet the sandboxed document themes itself from |
|
|
204
|
+
|
|
205
|
+
The stream ends by **closing the connection** after `RUN_FINISHED` or
|
|
206
|
+
`RUN_ERROR`. There is no `data: [DONE]` sentinel; emitting one makes the AG-UI
|
|
207
|
+
client error the subject, because it parses every `data:` line as JSON.
|
|
208
|
+
`: keepalive` comment frames are fine.
|
|
209
|
+
|
|
210
|
+
**Two server toggles must be on**, in every environment that runs an extension.
|
|
211
|
+
Both default to false, and while either is off its paths answer `404` before
|
|
212
|
+
authentication, which reads as "the agent does not exist":
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
cortenacore.http.endpoints.agui.enabled = true
|
|
216
|
+
cortenacore.http.endpoints.controlPlane.enabled = true
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### The MCP App frame
|
|
220
|
+
|
|
221
|
+
Rendering an extension's own screen means a sandboxed iframe, a `srcdoc` CSP, a
|
|
222
|
+
JSON-RPC postMessage bridge and a `tools/invoke` proxy holding a credential —
|
|
223
|
+
host concerns, not component ones. `renderToolCall` is the seam: it is handed
|
|
224
|
+
each tool call and the `ui://` resource its result carries, and a node it
|
|
225
|
+
returns replaces the default card.
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
<AgentChatPopup
|
|
229
|
+
extension={extension}
|
|
230
|
+
client={client}
|
|
231
|
+
renderToolCall={({ entry, mcpAppUri }) =>
|
|
232
|
+
mcpAppUri ? <McpAppFrame uri={mcpAppUri} result={entry.output} /> : undefined
|
|
233
|
+
}
|
|
234
|
+
/>
|
|
235
|
+
```
|
|
236
|
+
|
|
89
237
|
## Charts in tests
|
|
90
238
|
|
|
91
239
|
Both chart engines size themselves from the parent box, and under jsdom every
|
package/dist/a2ui/views.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
import { cn } from "../lib/cn.js";
|
|
4
4
|
import { Alert, AlertDescription, AlertIcon, AlertTitle } from "../components/alert.js";
|
|
5
|
-
import { Badge } from "../components/badge.js";
|
|
6
5
|
import { Button } from "../components/button.js";
|
|
6
|
+
import { Input } from "../components/input.js";
|
|
7
|
+
import { Badge } from "../components/badge.js";
|
|
7
8
|
import { ButtonLink } from "../components/button-link.js";
|
|
8
9
|
import { Card, CardContent } from "../components/card.js";
|
|
9
10
|
import { Checkbox } from "../components/checkbox.js";
|
|
10
|
-
import { Input } from "../components/input.js";
|
|
11
11
|
import { EmptyState } from "../components/empty-state.js";
|
|
12
12
|
import { Field, FieldDescription, FieldError, FieldLabel } from "../components/field.js";
|
|
13
13
|
import { Progress, ProgressLabel, ProgressValue } from "../components/progress.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/agent-chat/a2ui-block.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A2UI blocks as the chat carries them.
|
|
5
|
+
*
|
|
6
|
+
* Ported from `cortena-shared/src/a2ui/block.ts` (Cortena monorepo, DESIGN-35).
|
|
7
|
+
* `cortena-shared` is not published, so the parts this package needs are copied
|
|
8
|
+
* rather than imported; keep the two in step when either changes.
|
|
9
|
+
*
|
|
10
|
+
* An agent draws UI by writing a ```a2ui fenced block, cortenacore strips it out
|
|
11
|
+
* of the prose, and it reaches the client on a channel of its own — a
|
|
12
|
+
* `CUSTOM cortena.a2ui` event on the AG-UI transport. Both the streaming bubble
|
|
13
|
+
* and the finished message carry the result here, so the bubble has exactly one
|
|
14
|
+
* thing to render and no markdown to parse.
|
|
15
|
+
*
|
|
16
|
+
* Nothing in this module knows about React: the `messages` array is handed to
|
|
17
|
+
* `<A2UIRenderer message={...} />` verbatim, and folding a surface, validating a
|
|
18
|
+
* component and drawing it are the renderer's job.
|
|
19
|
+
*/
|
|
20
|
+
/** One block, as the transport delivers it. */
|
|
21
|
+
export interface A2UIChatBlock {
|
|
22
|
+
/**
|
|
23
|
+
* Accumulation key: the surface id the payload names, or a key private to the
|
|
24
|
+
* block when it names none. Two blocks with the same key are two pushes to one
|
|
25
|
+
* surface and fold together.
|
|
26
|
+
*/
|
|
27
|
+
surfaceId: string;
|
|
28
|
+
/** A2UI messages, in arrival order. */
|
|
29
|
+
messages: unknown[];
|
|
30
|
+
/** Where the block came from: assistant text, or a `canvas` tool push. */
|
|
31
|
+
source?: "text" | "canvas";
|
|
32
|
+
/** Assistant message the block belonged to, when the transport knows it. */
|
|
33
|
+
messageId?: string;
|
|
34
|
+
/** Set when the fence never closed or its body did not parse. */
|
|
35
|
+
error?: string;
|
|
36
|
+
/** The raw fence body; only present with `error`. */
|
|
37
|
+
raw?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Read one block out of an unknown wire value; `null` when it is not one. */
|
|
40
|
+
export declare function readA2UIBlock(value: unknown): A2UIChatBlock | null;
|
|
41
|
+
/**
|
|
42
|
+
* Pull the `a2ui` content parts out of a chat event message.
|
|
43
|
+
*
|
|
44
|
+
* A client that does not know the type ignores it, which is what lets the same
|
|
45
|
+
* message shape carry blocks to a reader that cannot draw them.
|
|
46
|
+
*/
|
|
47
|
+
export declare function extractA2UIBlocks(message?: {
|
|
48
|
+
content?: Array<Record<string, unknown>>;
|
|
49
|
+
} | null): A2UIChatBlock[];
|
|
50
|
+
/**
|
|
51
|
+
* Fold blocks that share a surface id into one, concatenating their messages.
|
|
52
|
+
*
|
|
53
|
+
* An agent that streams a surface writes the shell, then the rows, then a
|
|
54
|
+
* correction — three fences naming one surface. The renderer folds a message
|
|
55
|
+
* list onto a surface, so handing it the concatenation renders the finished
|
|
56
|
+
* surface; handing it three separate blocks would draw the shell three times.
|
|
57
|
+
*/
|
|
58
|
+
export declare function foldA2UIBlocks(blocks: readonly A2UIChatBlock[]): A2UIChatBlock[];
|
|
59
|
+
//#endregion
|
|
60
|
+
//# sourceMappingURL=a2ui-block.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/agent-chat/a2ui-block.ts
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
/** Read one block out of an unknown wire value; `null` when it is not one. */
|
|
7
|
+
function readA2UIBlock(value) {
|
|
8
|
+
if (!isRecord(value)) return null;
|
|
9
|
+
const messages = Array.isArray(value.messages) ? value.messages : [];
|
|
10
|
+
const surfaceId = typeof value.surfaceId === "string" && value.surfaceId ? value.surfaceId : "@default";
|
|
11
|
+
const error = typeof value.error === "string" && value.error ? value.error : void 0;
|
|
12
|
+
if (messages.length === 0 && !error) return null;
|
|
13
|
+
return {
|
|
14
|
+
surfaceId,
|
|
15
|
+
messages,
|
|
16
|
+
...value.source === "canvas" || value.source === "text" ? { source: value.source } : {},
|
|
17
|
+
...typeof value.messageId === "string" && value.messageId ? { messageId: value.messageId } : {},
|
|
18
|
+
...error ? { error } : {},
|
|
19
|
+
...typeof value.raw === "string" ? { raw: value.raw } : {}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pull the `a2ui` content parts out of a chat event message.
|
|
24
|
+
*
|
|
25
|
+
* A client that does not know the type ignores it, which is what lets the same
|
|
26
|
+
* message shape carry blocks to a reader that cannot draw them.
|
|
27
|
+
*/
|
|
28
|
+
function extractA2UIBlocks(message) {
|
|
29
|
+
const blocks = [];
|
|
30
|
+
for (const part of message?.content ?? []) {
|
|
31
|
+
if (!isRecord(part) || part.type !== "a2ui") continue;
|
|
32
|
+
const block = readA2UIBlock(part.a2ui ?? part);
|
|
33
|
+
if (block) blocks.push(block);
|
|
34
|
+
}
|
|
35
|
+
return foldA2UIBlocks(blocks);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fold blocks that share a surface id into one, concatenating their messages.
|
|
39
|
+
*
|
|
40
|
+
* An agent that streams a surface writes the shell, then the rows, then a
|
|
41
|
+
* correction — three fences naming one surface. The renderer folds a message
|
|
42
|
+
* list onto a surface, so handing it the concatenation renders the finished
|
|
43
|
+
* surface; handing it three separate blocks would draw the shell three times.
|
|
44
|
+
*/
|
|
45
|
+
function foldA2UIBlocks(blocks) {
|
|
46
|
+
const byId = /* @__PURE__ */ new Map();
|
|
47
|
+
const order = [];
|
|
48
|
+
for (const block of blocks) {
|
|
49
|
+
const existing = byId.get(block.surfaceId);
|
|
50
|
+
if (!existing) {
|
|
51
|
+
byId.set(block.surfaceId, {
|
|
52
|
+
...block,
|
|
53
|
+
messages: [...block.messages]
|
|
54
|
+
});
|
|
55
|
+
order.push(block.surfaceId);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
existing.messages.push(...block.messages);
|
|
59
|
+
if (block.error && !existing.error) {
|
|
60
|
+
existing.error = block.error;
|
|
61
|
+
if (block.raw !== void 0) existing.raw = block.raw;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return order.map((id) => byId.get(id)).filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { extractA2UIBlocks, foldA2UIBlocks, readA2UIBlock };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=a2ui-block.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a2ui-block.js","names":[],"sources":["../../src/agent-chat/a2ui-block.ts"],"sourcesContent":["/**\n * A2UI blocks as the chat carries them.\n *\n * Ported from `cortena-shared/src/a2ui/block.ts` (Cortena monorepo, DESIGN-35).\n * `cortena-shared` is not published, so the parts this package needs are copied\n * rather than imported; keep the two in step when either changes.\n *\n * An agent draws UI by writing a ```a2ui fenced block, cortenacore strips it out\n * of the prose, and it reaches the client on a channel of its own — a\n * `CUSTOM cortena.a2ui` event on the AG-UI transport. Both the streaming bubble\n * and the finished message carry the result here, so the bubble has exactly one\n * thing to render and no markdown to parse.\n *\n * Nothing in this module knows about React: the `messages` array is handed to\n * `<A2UIRenderer message={...} />` verbatim, and folding a surface, validating a\n * component and drawing it are the renderer's job.\n */\n\n/** One block, as the transport delivers it. */\nexport interface A2UIChatBlock {\n /**\n * Accumulation key: the surface id the payload names, or a key private to the\n * block when it names none. Two blocks with the same key are two pushes to one\n * surface and fold together.\n */\n surfaceId: string;\n /** A2UI messages, in arrival order. */\n messages: unknown[];\n /** Where the block came from: assistant text, or a `canvas` tool push. */\n source?: \"text\" | \"canvas\";\n /** Assistant message the block belonged to, when the transport knows it. */\n messageId?: string;\n /** Set when the fence never closed or its body did not parse. */\n error?: string;\n /** The raw fence body; only present with `error`. */\n raw?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Read one block out of an unknown wire value; `null` when it is not one. */\nexport function readA2UIBlock(value: unknown): A2UIChatBlock | null {\n if (!isRecord(value)) {\n return null;\n }\n const messages = Array.isArray(value.messages) ? value.messages : [];\n const surfaceId =\n typeof value.surfaceId === \"string\" && value.surfaceId ? value.surfaceId : \"@default\";\n const error = typeof value.error === \"string\" && value.error ? value.error : undefined;\n if (messages.length === 0 && !error) {\n return null;\n }\n return {\n surfaceId,\n messages,\n ...(value.source === \"canvas\" || value.source === \"text\" ? { source: value.source } : {}),\n ...(typeof value.messageId === \"string\" && value.messageId\n ? { messageId: value.messageId }\n : {}),\n ...(error ? { error } : {}),\n ...(typeof value.raw === \"string\" ? { raw: value.raw } : {}),\n };\n}\n\n/**\n * Pull the `a2ui` content parts out of a chat event message.\n *\n * A client that does not know the type ignores it, which is what lets the same\n * message shape carry blocks to a reader that cannot draw them.\n */\nexport function extractA2UIBlocks(\n message?: { content?: Array<Record<string, unknown>> } | null,\n): A2UIChatBlock[] {\n const blocks: A2UIChatBlock[] = [];\n for (const part of message?.content ?? []) {\n if (!isRecord(part) || part.type !== \"a2ui\") {\n continue;\n }\n const block = readA2UIBlock(part.a2ui ?? part);\n if (block) {\n blocks.push(block);\n }\n }\n return foldA2UIBlocks(blocks);\n}\n\n/**\n * Fold blocks that share a surface id into one, concatenating their messages.\n *\n * An agent that streams a surface writes the shell, then the rows, then a\n * correction — three fences naming one surface. The renderer folds a message\n * list onto a surface, so handing it the concatenation renders the finished\n * surface; handing it three separate blocks would draw the shell three times.\n */\nexport function foldA2UIBlocks(blocks: readonly A2UIChatBlock[]): A2UIChatBlock[] {\n const byId = new Map<string, A2UIChatBlock>();\n const order: string[] = [];\n for (const block of blocks) {\n const existing = byId.get(block.surfaceId);\n if (!existing) {\n byId.set(block.surfaceId, { ...block, messages: [...block.messages] });\n order.push(block.surfaceId);\n continue;\n }\n existing.messages.push(...block.messages);\n // The first failure is the one worth showing: later ones are usually the\n // same malformed payload arriving again.\n if (block.error && !existing.error) {\n existing.error = block.error;\n if (block.raw !== undefined) {\n existing.raw = block.raw;\n }\n }\n }\n return order.map((id) => byId.get(id)!).filter(Boolean);\n}\n"],"mappings":";;AAsCA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;AAGA,SAAgB,cAAc,OAAsC;CAClE,IAAI,CAAC,SAAS,KAAK,GACjB,OAAO;CAET,MAAM,WAAW,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,WAAW,CAAC;CACnE,MAAM,YACJ,OAAO,MAAM,cAAc,YAAY,MAAM,YAAY,MAAM,YAAY;CAC7E,MAAM,QAAQ,OAAO,MAAM,UAAU,YAAY,MAAM,QAAQ,MAAM,QAAQ,KAAA;CAC7E,IAAI,SAAS,WAAW,KAAK,CAAC,OAC5B,OAAO;CAET,OAAO;EACL;EACA;EACA,GAAI,MAAM,WAAW,YAAY,MAAM,WAAW,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;EACvF,GAAI,OAAO,MAAM,cAAc,YAAY,MAAM,YAC7C,EAAE,WAAW,MAAM,UAAU,IAC7B,CAAC;EACL,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;EACzB,GAAI,OAAO,MAAM,QAAQ,WAAW,EAAE,KAAK,MAAM,IAAI,IAAI,CAAC;CAC5D;AACF;;;;;;;AAQA,SAAgB,kBACd,SACiB;CACjB,MAAM,SAA0B,CAAC;CACjC,KAAK,MAAM,QAAQ,SAAS,WAAW,CAAC,GAAG;EACzC,IAAI,CAAC,SAAS,IAAI,KAAK,KAAK,SAAS,QACnC;EAEF,MAAM,QAAQ,cAAc,KAAK,QAAQ,IAAI;EAC7C,IAAI,OACF,OAAO,KAAK,KAAK;CAErB;CACA,OAAO,eAAe,MAAM;AAC9B;;;;;;;;;AAUA,SAAgB,eAAe,QAAmD;CAChF,MAAM,uBAAO,IAAI,IAA2B;CAC5C,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,KAAK,IAAI,MAAM,SAAS;EACzC,IAAI,CAAC,UAAU;GACb,KAAK,IAAI,MAAM,WAAW;IAAE,GAAG;IAAO,UAAU,CAAC,GAAG,MAAM,QAAQ;GAAE,CAAC;GACrE,MAAM,KAAK,MAAM,SAAS;GAC1B;EACF;EACA,SAAS,SAAS,KAAK,GAAG,MAAM,QAAQ;EAGxC,IAAI,MAAM,SAAS,CAAC,SAAS,OAAO;GAClC,SAAS,QAAQ,MAAM;GACvB,IAAI,MAAM,QAAQ,KAAA,GAChB,SAAS,MAAM,MAAM;EAEzB;CACF;CACA,OAAO,MAAM,KAAK,OAAO,KAAK,IAAI,EAAE,CAAE,CAAC,CAAC,OAAO,OAAO;AACxD"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { AgentChatClient } from "./types.js";
|
|
3
|
+
//#region src/agent-chat/agui-client.d.ts
|
|
4
|
+
export interface CreateAguiAgentChatClientOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Origin the AG-UI and control-plane paths hang off, no trailing slash.
|
|
7
|
+
* For an extension this is its own gateway, which proxies to cortenacore.
|
|
8
|
+
*/
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/** The signed-in USER's JWT. Read on every request, never captured once. */
|
|
11
|
+
getToken: () => string;
|
|
12
|
+
/** The agent template slug — the extension id. Sessions are `agent:<id>:…`. */
|
|
13
|
+
agentId: string;
|
|
14
|
+
fetchImpl?: typeof fetch;
|
|
15
|
+
/** Frontend tools advertised on every run; AG-UI requires a description. */
|
|
16
|
+
tools?: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
parameters?: unknown;
|
|
20
|
+
}>;
|
|
21
|
+
/** Backoff before re-attaching a dropped stream. */
|
|
22
|
+
reconnectDelays?: readonly number[];
|
|
23
|
+
maxReconnectAttempts?: number;
|
|
24
|
+
sleep?: (ms: number) => Promise<void>;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* An `AgentChatClient` that streams over AG-UI and reads sessions and history
|
|
29
|
+
* over the REST control plane.
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* const client = React.useMemo(
|
|
33
|
+
* () => createAguiAgentChatClient({ baseUrl: "/api/agent", getToken: () => jwt, agentId: "tasks" }),
|
|
34
|
+
* [jwt],
|
|
35
|
+
* );
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function createAguiAgentChatClient(options: CreateAguiAgentChatClientOptions): AgentChatClient;
|
|
39
|
+
//#endregion
|
|
40
|
+
//# sourceMappingURL=agui-client.d.ts.map
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { AgentChatBridge } from "./bridge.js";
|
|
3
|
+
//#region src/agent-chat/agui-client.ts
|
|
4
|
+
const DEFAULT_RECONNECT_DELAYS = [
|
|
5
|
+
500,
|
|
6
|
+
1e3,
|
|
7
|
+
2e3,
|
|
8
|
+
5e3,
|
|
9
|
+
1e4
|
|
10
|
+
];
|
|
11
|
+
const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
|
|
12
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
/**
|
|
14
|
+
* What a relative `baseUrl` resolves against.
|
|
15
|
+
*
|
|
16
|
+
* The browser's own origin, and a placeholder under Node — where this module
|
|
17
|
+
* is exercised by tests with a `fetchImpl` stub and there is no origin to
|
|
18
|
+
* speak of. The placeholder never reaches the network: the stub sees the
|
|
19
|
+
* resolved string, and `/api/core/...` is what it asserts on.
|
|
20
|
+
*/
|
|
21
|
+
function requestBase() {
|
|
22
|
+
return typeof location === "undefined" ? "http://cortena.invalid" : location.origin;
|
|
23
|
+
}
|
|
24
|
+
const MISSING_AGUI_PEER = "cortena-ui/agent-chat: the AG-UI transport needs the optional peer \"@ag-ui/client\". Install it (pnpm add @ag-ui/client), or pass your own AgentChatClient to AgentChat.";
|
|
25
|
+
/**
|
|
26
|
+
* `@ag-ui/client`, on demand.
|
|
27
|
+
*
|
|
28
|
+
* The specifier is a literal so a bundler still sees the dependency and puts
|
|
29
|
+
* it in a chunk of its own — `test/bundle-probe.test.mjs` measures that the
|
|
30
|
+
* agent-chat entry carries it. What changes is *when* it is evaluated, and
|
|
31
|
+
* what happens when it is not there at all.
|
|
32
|
+
*/
|
|
33
|
+
async function loadHttpAgent() {
|
|
34
|
+
try {
|
|
35
|
+
return (await import("@ag-ui/client")).HttpAgent;
|
|
36
|
+
} catch (cause) {
|
|
37
|
+
throw new Error(MISSING_AGUI_PEER, { cause });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var AguiAgentChatClient = class {
|
|
41
|
+
agentId;
|
|
42
|
+
opts;
|
|
43
|
+
delays;
|
|
44
|
+
maxAttempts;
|
|
45
|
+
sleep;
|
|
46
|
+
chatSinks = /* @__PURE__ */ new Set();
|
|
47
|
+
toolSinks = /* @__PURE__ */ new Set();
|
|
48
|
+
approvalSinks = /* @__PURE__ */ new Set();
|
|
49
|
+
active = null;
|
|
50
|
+
/** The optional peer, requested when the client is created and awaited per run. */
|
|
51
|
+
httpAgent;
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
this.opts = opts;
|
|
54
|
+
this.agentId = opts.agentId;
|
|
55
|
+
this.delays = opts.reconnectDelays ?? DEFAULT_RECONNECT_DELAYS;
|
|
56
|
+
this.maxAttempts = opts.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
57
|
+
this.sleep = opts.sleep ?? defaultSleep;
|
|
58
|
+
this.httpAgent = loadHttpAgent();
|
|
59
|
+
this.httpAgent.catch(() => {});
|
|
60
|
+
}
|
|
61
|
+
onChat(sink) {
|
|
62
|
+
this.chatSinks.add(sink);
|
|
63
|
+
return () => this.chatSinks.delete(sink);
|
|
64
|
+
}
|
|
65
|
+
onTool(sink) {
|
|
66
|
+
this.toolSinks.add(sink);
|
|
67
|
+
return () => this.toolSinks.delete(sink);
|
|
68
|
+
}
|
|
69
|
+
onApproval(sink) {
|
|
70
|
+
this.approvalSinks.add(sink);
|
|
71
|
+
return () => this.approvalSinks.delete(sink);
|
|
72
|
+
}
|
|
73
|
+
get runUrl() {
|
|
74
|
+
return `${this.opts.baseUrl}/agui/run`;
|
|
75
|
+
}
|
|
76
|
+
get abortUrl() {
|
|
77
|
+
return `${this.opts.baseUrl}/agui/abort`;
|
|
78
|
+
}
|
|
79
|
+
get approvalUrl() {
|
|
80
|
+
return `${this.opts.baseUrl}/agui/approval`;
|
|
81
|
+
}
|
|
82
|
+
headers() {
|
|
83
|
+
return {
|
|
84
|
+
authorization: `Bearer ${this.opts.getToken()}`,
|
|
85
|
+
"content-type": "application/json"
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
get fetch() {
|
|
89
|
+
return this.opts.fetchImpl ?? globalThis.fetch;
|
|
90
|
+
}
|
|
91
|
+
/** `sessions.list` and `chat.history` over the REST control plane. */
|
|
92
|
+
async request(method, params) {
|
|
93
|
+
const path = `${this.opts.baseUrl}/api/core/${method.split(".").join("/")}`;
|
|
94
|
+
const readOnly = method === "sessions.list" || method === "chat.history";
|
|
95
|
+
const url = new URL(path, requestBase());
|
|
96
|
+
if (readOnly) {
|
|
97
|
+
for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
98
|
+
}
|
|
99
|
+
const response = await this.fetch(url.toString(), {
|
|
100
|
+
method: readOnly ? "GET" : "POST",
|
|
101
|
+
headers: this.headers(),
|
|
102
|
+
...readOnly ? {} : { body: JSON.stringify(params) }
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) throw new Error(await readError(response, method));
|
|
105
|
+
return await response.json();
|
|
106
|
+
}
|
|
107
|
+
async send(input) {
|
|
108
|
+
this.startRun(input);
|
|
109
|
+
}
|
|
110
|
+
async startRun(input) {
|
|
111
|
+
const bridge = new AgentChatBridge({
|
|
112
|
+
runId: input.runId,
|
|
113
|
+
sessionKey: input.sessionKey,
|
|
114
|
+
chat: (event) => this.emit(this.chatSinks, event),
|
|
115
|
+
tool: (event) => this.emit(this.toolSinks, event),
|
|
116
|
+
approval: (event) => this.emit(this.approvalSinks, event),
|
|
117
|
+
...this.opts.now ? { now: this.opts.now } : {}
|
|
118
|
+
});
|
|
119
|
+
const forwarded = {
|
|
120
|
+
...input.thinking ? { thinking: input.thinking } : {},
|
|
121
|
+
...input.attachments?.length ? { attachments: input.attachments } : {},
|
|
122
|
+
...input.model ? { model: input.model } : {},
|
|
123
|
+
...input.a2uiAction ? { a2uiAction: input.a2uiAction } : {},
|
|
124
|
+
...input.mcpAppAction ? { mcpAppAction: input.mcpAppAction } : {}
|
|
125
|
+
};
|
|
126
|
+
let Agent;
|
|
127
|
+
try {
|
|
128
|
+
Agent = await this.httpAgent;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
bridge.fail(err instanceof Error ? err.message : MISSING_AGUI_PEER);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
let attempt = 0;
|
|
134
|
+
let resume = false;
|
|
135
|
+
for (;;) {
|
|
136
|
+
const runId = resume ? `${input.runId}:resume-${attempt}` : input.runId;
|
|
137
|
+
const agent = new Agent({
|
|
138
|
+
url: this.runUrl,
|
|
139
|
+
headers: this.headers(),
|
|
140
|
+
threadId: input.sessionKey,
|
|
141
|
+
initialMessages: resume ? [] : [{
|
|
142
|
+
id: input.runId,
|
|
143
|
+
role: "user",
|
|
144
|
+
content: input.message
|
|
145
|
+
}],
|
|
146
|
+
...this.opts.fetchImpl ? { fetch: this.opts.fetchImpl } : {}
|
|
147
|
+
});
|
|
148
|
+
this.active = {
|
|
149
|
+
runId: input.runId,
|
|
150
|
+
sessionKey: input.sessionKey,
|
|
151
|
+
bridge,
|
|
152
|
+
agent,
|
|
153
|
+
aborted: this.active?.aborted === true && this.active.runId === input.runId
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
await agent.runAgent({
|
|
157
|
+
runId,
|
|
158
|
+
...this.opts.tools?.length ? { tools: this.opts.tools } : {},
|
|
159
|
+
forwardedProps: {
|
|
160
|
+
...forwarded,
|
|
161
|
+
...resume ? { resumeRunId: input.runId } : {}
|
|
162
|
+
}
|
|
163
|
+
}, { onEvent: ({ event }) => bridge.handleEvent(event) });
|
|
164
|
+
return;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (bridge.finished || this.active?.aborted) return;
|
|
167
|
+
attempt += 1;
|
|
168
|
+
if (attempt > this.maxAttempts) {
|
|
169
|
+
bridge.fail(err instanceof Error ? err.message : "The agent stream failed");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
await this.sleep(this.delays[Math.min(attempt - 1, this.delays.length - 1)] ?? 1e3);
|
|
173
|
+
if (bridge.finished || this.active?.aborted) return;
|
|
174
|
+
resume = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async abort(params) {
|
|
179
|
+
const active = this.active;
|
|
180
|
+
if (active) {
|
|
181
|
+
active.aborted = true;
|
|
182
|
+
try {
|
|
183
|
+
active.agent.abortRun();
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
const runId = params?.runId ?? active?.runId;
|
|
187
|
+
const threadId = params?.sessionKey ?? active?.sessionKey;
|
|
188
|
+
if (!runId || !threadId) return;
|
|
189
|
+
try {
|
|
190
|
+
await this.fetch(this.abortUrl, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: this.headers(),
|
|
193
|
+
body: JSON.stringify({
|
|
194
|
+
runId,
|
|
195
|
+
threadId
|
|
196
|
+
})
|
|
197
|
+
});
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Answer one approval, on the thread and run it arrived on.
|
|
202
|
+
*
|
|
203
|
+
* `this.active` is deliberately not consulted. An approval pauses a run, and
|
|
204
|
+
* a user is free to open another session from the drawer or start another
|
|
205
|
+
* turn while it waits — at which point `active.sessionKey` is a different
|
|
206
|
+
* conversation and "allow" would be sent against it. The request carried its
|
|
207
|
+
* own `{ runId, sessionKey }`; those are what go on the wire.
|
|
208
|
+
*/
|
|
209
|
+
async resolveApproval(id, decision, context) {
|
|
210
|
+
const response = await this.fetch(this.approvalUrl, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: this.headers(),
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
id,
|
|
215
|
+
decision,
|
|
216
|
+
threadId: context.sessionKey,
|
|
217
|
+
runId: context.runId
|
|
218
|
+
})
|
|
219
|
+
});
|
|
220
|
+
if (!response.ok) throw new Error(await readError(response, "approval"));
|
|
221
|
+
}
|
|
222
|
+
emit(sinks, event) {
|
|
223
|
+
for (const sink of [...sinks]) sink(event);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* An `AgentChatClient` that streams over AG-UI and reads sessions and history
|
|
228
|
+
* over the REST control plane.
|
|
229
|
+
*
|
|
230
|
+
* ```tsx
|
|
231
|
+
* const client = React.useMemo(
|
|
232
|
+
* () => createAguiAgentChatClient({ baseUrl: "/api/agent", getToken: () => jwt, agentId: "tasks" }),
|
|
233
|
+
* [jwt],
|
|
234
|
+
* );
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
function createAguiAgentChatClient(options) {
|
|
238
|
+
return new AguiAgentChatClient(options);
|
|
239
|
+
}
|
|
240
|
+
async function readError(response, method) {
|
|
241
|
+
try {
|
|
242
|
+
const body = await response.json();
|
|
243
|
+
if (body?.error?.message) return body.error.message;
|
|
244
|
+
} catch {}
|
|
245
|
+
if (response.status === 404) return `${method} is unavailable (404). Check that the AG-UI and control-plane endpoints are enabled.`;
|
|
246
|
+
return `${method} failed (${response.status})`;
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
export { MISSING_AGUI_PEER, createAguiAgentChatClient };
|
|
250
|
+
|
|
251
|
+
//# sourceMappingURL=agui-client.js.map
|