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
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AgentChatClient` over AG-UI.
|
|
3
|
+
*
|
|
4
|
+
* Ported from `cortena-shared/src/agui/client.ts` (Cortena monorepo,
|
|
5
|
+
* DESIGN-37). `cortena-shared` is not published, so what this package needs is
|
|
6
|
+
* copied rather than imported; keep the two in step.
|
|
7
|
+
*
|
|
8
|
+
* **This is the only module in cortena-ui that imports `@ag-ui/client`**, and
|
|
9
|
+
* `cortena-ui/agent-chat` is the only entry that reaches it. `@ag-ui/client`
|
|
10
|
+
* brings rxjs, zod 3, uuid and protobuf; `test/bundle-probe.test.mjs` asserts
|
|
11
|
+
* none of it is reachable from `cortena-ui` or `cortena-ui/core`.
|
|
12
|
+
*
|
|
13
|
+
* It is an OPTIONAL peer, and the import is therefore **lazy**: a static
|
|
14
|
+
* `import { HttpAgent }` made the whole `cortena-ui/agent-chat` entry fail to
|
|
15
|
+
* load for a consumer that had not installed it, so an extension on another
|
|
16
|
+
* transport could not draw `AgentChatPopup` at all — a package it never asked
|
|
17
|
+
* for decided whether its chat rendered. Loading it inside the factory keeps
|
|
18
|
+
* the entry resolvable, and moves the failure to the one operation that
|
|
19
|
+
* genuinely needs the peer, with a message that says what to install.
|
|
20
|
+
*
|
|
21
|
+
* What an extension must expose for this to work is in README.md, "The agent
|
|
22
|
+
* pop-up": `POST /agui/run`, `/agui/abort`, `/agui/approval` and the two
|
|
23
|
+
* control-plane reads, all proxied through the extension's own gateway with the
|
|
24
|
+
* signed-in user's JWT — never a service token.
|
|
25
|
+
*
|
|
26
|
+
* Two things about the wire that are easy to get wrong:
|
|
27
|
+
*
|
|
28
|
+
* - The stream ends by **closing the connection** after `RUN_FINISHED` or
|
|
29
|
+
* `RUN_ERROR`. There is no `data: [DONE]` sentinel; emitting one makes the
|
|
30
|
+
* AG-UI client error the subject, because it `JSON.parse`s every `data:` line.
|
|
31
|
+
* - Approvals go to their own endpoint. Sending a decision as `state` on a
|
|
32
|
+
* second `/agui/run` opens a second concurrent stream, and two mappers then
|
|
33
|
+
* write two sets of message ids into one bridge, which resets its text buffer
|
|
34
|
+
* whenever the message id changes.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { HttpAgent } from "@ag-ui/client";
|
|
38
|
+
import { AgentChatBridge } from "./bridge";
|
|
39
|
+
import type {
|
|
40
|
+
AgentApprovalContext,
|
|
41
|
+
AgentApprovalDecision,
|
|
42
|
+
AgentApprovalEvent,
|
|
43
|
+
AgentChatClient,
|
|
44
|
+
AgentChatEvent,
|
|
45
|
+
AgentChatSendInput,
|
|
46
|
+
AgentToolEvent,
|
|
47
|
+
} from "./types";
|
|
48
|
+
|
|
49
|
+
export interface CreateAguiAgentChatClientOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Origin the AG-UI and control-plane paths hang off, no trailing slash.
|
|
52
|
+
* For an extension this is its own gateway, which proxies to cortenacore.
|
|
53
|
+
*/
|
|
54
|
+
baseUrl: string;
|
|
55
|
+
/** The signed-in USER's JWT. Read on every request, never captured once. */
|
|
56
|
+
getToken: () => string;
|
|
57
|
+
/** The agent template slug — the extension id. Sessions are `agent:<id>:…`. */
|
|
58
|
+
agentId: string;
|
|
59
|
+
fetchImpl?: typeof fetch;
|
|
60
|
+
/** Frontend tools advertised on every run; AG-UI requires a description. */
|
|
61
|
+
tools?: Array<{ name: string; description: string; parameters?: unknown }>;
|
|
62
|
+
/** Backoff before re-attaching a dropped stream. */
|
|
63
|
+
reconnectDelays?: readonly number[];
|
|
64
|
+
maxReconnectAttempts?: number;
|
|
65
|
+
sleep?: (ms: number) => Promise<void>;
|
|
66
|
+
now?: () => number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DEFAULT_RECONNECT_DELAYS: readonly number[] = [500, 1_000, 2_000, 5_000, 10_000];
|
|
70
|
+
const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
|
|
71
|
+
|
|
72
|
+
const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What a relative `baseUrl` resolves against.
|
|
76
|
+
*
|
|
77
|
+
* The browser's own origin, and a placeholder under Node — where this module
|
|
78
|
+
* is exercised by tests with a `fetchImpl` stub and there is no origin to
|
|
79
|
+
* speak of. The placeholder never reaches the network: the stub sees the
|
|
80
|
+
* resolved string, and `/api/core/...` is what it asserts on.
|
|
81
|
+
*/
|
|
82
|
+
function requestBase(): string {
|
|
83
|
+
return typeof location === "undefined" ? "http://cortena.invalid" : location.origin;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What `@ag-ui/client` exports and this module constructs. */
|
|
87
|
+
type HttpAgentConstructor = typeof import("@ag-ui/client").HttpAgent;
|
|
88
|
+
|
|
89
|
+
export const MISSING_AGUI_PEER =
|
|
90
|
+
'cortena-ui/agent-chat: the AG-UI transport needs the optional peer "@ag-ui/client". ' +
|
|
91
|
+
"Install it (pnpm add @ag-ui/client), or pass your own AgentChatClient to AgentChat.";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `@ag-ui/client`, on demand.
|
|
95
|
+
*
|
|
96
|
+
* The specifier is a literal so a bundler still sees the dependency and puts
|
|
97
|
+
* it in a chunk of its own — `test/bundle-probe.test.mjs` measures that the
|
|
98
|
+
* agent-chat entry carries it. What changes is *when* it is evaluated, and
|
|
99
|
+
* what happens when it is not there at all.
|
|
100
|
+
*/
|
|
101
|
+
async function loadHttpAgent(): Promise<HttpAgentConstructor> {
|
|
102
|
+
try {
|
|
103
|
+
return (await import("@ag-ui/client")).HttpAgent;
|
|
104
|
+
} catch (cause) {
|
|
105
|
+
throw new Error(MISSING_AGUI_PEER, { cause });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type Sink<T> = (event: T) => void;
|
|
110
|
+
|
|
111
|
+
interface ActiveRun {
|
|
112
|
+
runId: string;
|
|
113
|
+
sessionKey: string;
|
|
114
|
+
bridge: AgentChatBridge;
|
|
115
|
+
agent: HttpAgent;
|
|
116
|
+
aborted: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class AguiAgentChatClient implements AgentChatClient {
|
|
120
|
+
readonly agentId: string;
|
|
121
|
+
|
|
122
|
+
private readonly opts: CreateAguiAgentChatClientOptions;
|
|
123
|
+
private readonly delays: readonly number[];
|
|
124
|
+
private readonly maxAttempts: number;
|
|
125
|
+
private readonly sleep: (ms: number) => Promise<void>;
|
|
126
|
+
private readonly chatSinks = new Set<Sink<AgentChatEvent>>();
|
|
127
|
+
private readonly toolSinks = new Set<Sink<AgentToolEvent>>();
|
|
128
|
+
private readonly approvalSinks = new Set<Sink<AgentApprovalEvent>>();
|
|
129
|
+
private active: ActiveRun | null = null;
|
|
130
|
+
/** The optional peer, requested when the client is created and awaited per run. */
|
|
131
|
+
private readonly httpAgent: Promise<HttpAgentConstructor>;
|
|
132
|
+
|
|
133
|
+
constructor(opts: CreateAguiAgentChatClientOptions) {
|
|
134
|
+
this.opts = opts;
|
|
135
|
+
this.agentId = opts.agentId;
|
|
136
|
+
this.delays = opts.reconnectDelays ?? DEFAULT_RECONNECT_DELAYS;
|
|
137
|
+
this.maxAttempts = opts.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
138
|
+
this.sleep = opts.sleep ?? defaultSleep;
|
|
139
|
+
// Started here so a missing peer is discovered when the client is made
|
|
140
|
+
// rather than mid-stream. The rejection is answered immediately: a client
|
|
141
|
+
// created and never sent to must not raise an unhandled rejection, and
|
|
142
|
+
// `startRun` awaits the same promise where the user can be told.
|
|
143
|
+
this.httpAgent = loadHttpAgent();
|
|
144
|
+
this.httpAgent.catch(() => {});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onChat(sink: Sink<AgentChatEvent>): () => void {
|
|
148
|
+
this.chatSinks.add(sink);
|
|
149
|
+
return () => this.chatSinks.delete(sink);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
onTool(sink: Sink<AgentToolEvent>): () => void {
|
|
153
|
+
this.toolSinks.add(sink);
|
|
154
|
+
return () => this.toolSinks.delete(sink);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
onApproval(sink: Sink<AgentApprovalEvent>): () => void {
|
|
158
|
+
this.approvalSinks.add(sink);
|
|
159
|
+
return () => this.approvalSinks.delete(sink);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get runUrl(): string {
|
|
163
|
+
return `${this.opts.baseUrl}/agui/run`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get abortUrl(): string {
|
|
167
|
+
return `${this.opts.baseUrl}/agui/abort`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get approvalUrl(): string {
|
|
171
|
+
return `${this.opts.baseUrl}/agui/approval`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private headers(): Record<string, string> {
|
|
175
|
+
return {
|
|
176
|
+
authorization: `Bearer ${this.opts.getToken()}`,
|
|
177
|
+
"content-type": "application/json",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private get fetch(): typeof fetch {
|
|
182
|
+
return this.opts.fetchImpl ?? globalThis.fetch;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `sessions.list` and `chat.history` over the REST control plane. */
|
|
186
|
+
async request<T = unknown>(method: string, params: Record<string, unknown>): Promise<T> {
|
|
187
|
+
const path = `${this.opts.baseUrl}/api/core/${method.split(".").join("/")}`;
|
|
188
|
+
const readOnly = method === "sessions.list" || method === "chat.history";
|
|
189
|
+
// `new URL(path)` with no base throws on a relative `baseUrl`, and the one
|
|
190
|
+
// in this module's own example is `"/api/agent"` — the shape every
|
|
191
|
+
// extension uses, since the gateway that proxies these calls is the app's
|
|
192
|
+
// own origin. `sessions.list` therefore threw before it was ever sent, and
|
|
193
|
+
// the chat opened on "The agent could not be reached".
|
|
194
|
+
const url = new URL(path, requestBase());
|
|
195
|
+
if (readOnly) {
|
|
196
|
+
for (const [key, value] of Object.entries(params)) {
|
|
197
|
+
if (value !== undefined && value !== null) {
|
|
198
|
+
url.searchParams.set(key, String(value));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const response = await this.fetch(url.toString(), {
|
|
203
|
+
method: readOnly ? "GET" : "POST",
|
|
204
|
+
headers: this.headers(),
|
|
205
|
+
...(readOnly ? {} : { body: JSON.stringify(params) }),
|
|
206
|
+
});
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
throw new Error(await readError(response, method));
|
|
209
|
+
}
|
|
210
|
+
return (await response.json()) as T;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async send(input: AgentChatSendInput): Promise<void> {
|
|
214
|
+
// Fire and forget: the caller only awaits the acceptance. Events follow on
|
|
215
|
+
// the stream, exactly as they do when the server acknowledges a send.
|
|
216
|
+
void this.startRun(input);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private async startRun(input: AgentChatSendInput): Promise<void> {
|
|
220
|
+
const bridge = new AgentChatBridge({
|
|
221
|
+
runId: input.runId,
|
|
222
|
+
sessionKey: input.sessionKey,
|
|
223
|
+
chat: (event) => this.emit(this.chatSinks, event),
|
|
224
|
+
tool: (event) => this.emit(this.toolSinks, event),
|
|
225
|
+
approval: (event) => this.emit(this.approvalSinks, event),
|
|
226
|
+
...(this.opts.now ? { now: this.opts.now } : {}),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const forwarded: Record<string, unknown> = {
|
|
230
|
+
...(input.thinking ? { thinking: input.thinking } : {}),
|
|
231
|
+
...(input.attachments?.length ? { attachments: input.attachments } : {}),
|
|
232
|
+
...(input.model ? { model: input.model } : {}),
|
|
233
|
+
// A click inside a rendered A2UI surface or MCP App. Structured rather
|
|
234
|
+
// than typed prose: cortenacore turns it into the documented envelope.
|
|
235
|
+
...(input.a2uiAction ? { a2uiAction: input.a2uiAction } : {}),
|
|
236
|
+
...(input.mcpAppAction ? { mcpAppAction: input.mcpAppAction } : {}),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
let Agent: HttpAgentConstructor;
|
|
240
|
+
try {
|
|
241
|
+
Agent = await this.httpAgent;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// The one operation that needs the peer, and the only place it can be
|
|
244
|
+
// reported: in the chat, as a failed run, rather than as a rejected
|
|
245
|
+
// promise nobody is holding.
|
|
246
|
+
bridge.fail(err instanceof Error ? err.message : MISSING_AGUI_PEER);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let attempt = 0;
|
|
251
|
+
let resume = false;
|
|
252
|
+
for (;;) {
|
|
253
|
+
const runId = resume ? `${input.runId}:resume-${attempt}` : input.runId;
|
|
254
|
+
const agent = new Agent({
|
|
255
|
+
url: this.runUrl,
|
|
256
|
+
headers: this.headers(),
|
|
257
|
+
threadId: input.sessionKey,
|
|
258
|
+
// A re-attach carries no new user message; the server re-streams the
|
|
259
|
+
// run named by forwardedProps.resumeRunId instead of starting one.
|
|
260
|
+
initialMessages: resume
|
|
261
|
+
? []
|
|
262
|
+
: [{ id: input.runId, role: "user", content: input.message }],
|
|
263
|
+
...(this.opts.fetchImpl ? { fetch: this.opts.fetchImpl } : {}),
|
|
264
|
+
});
|
|
265
|
+
this.active = {
|
|
266
|
+
runId: input.runId,
|
|
267
|
+
sessionKey: input.sessionKey,
|
|
268
|
+
bridge,
|
|
269
|
+
agent,
|
|
270
|
+
aborted: this.active?.aborted === true && this.active.runId === input.runId,
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
await agent.runAgent(
|
|
274
|
+
{
|
|
275
|
+
runId,
|
|
276
|
+
...(this.opts.tools?.length ? { tools: this.opts.tools } : {}),
|
|
277
|
+
forwardedProps: {
|
|
278
|
+
...forwarded,
|
|
279
|
+
...(resume ? { resumeRunId: input.runId } : {}),
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{ onEvent: ({ event }) => bridge.handleEvent(event as never) },
|
|
283
|
+
);
|
|
284
|
+
return;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (bridge.finished || this.active?.aborted) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
attempt += 1;
|
|
290
|
+
if (attempt > this.maxAttempts) {
|
|
291
|
+
bridge.fail(err instanceof Error ? err.message : "The agent stream failed");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
await this.sleep(this.delays[Math.min(attempt - 1, this.delays.length - 1)] ?? 1_000);
|
|
295
|
+
// The backoff is up to ten seconds long, and everything can change in
|
|
296
|
+
// it: the user presses Stop, or a late RUN_FINISHED lands on the
|
|
297
|
+
// bridge. Checking only before the sleep re-attached to a run that had
|
|
298
|
+
// already ended — a second stream, a second set of message ids into
|
|
299
|
+
// one bridge, and an abort the user watched do nothing.
|
|
300
|
+
if (bridge.finished || this.active?.aborted) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
resume = true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async abort(params?: { runId?: string; sessionKey?: string }): Promise<void> {
|
|
309
|
+
const active = this.active;
|
|
310
|
+
if (active) {
|
|
311
|
+
active.aborted = true;
|
|
312
|
+
try {
|
|
313
|
+
active.agent.abortRun();
|
|
314
|
+
} catch {
|
|
315
|
+
// Already finished; the POST below is still worth making.
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const runId = params?.runId ?? active?.runId;
|
|
319
|
+
const threadId = params?.sessionKey ?? active?.sessionKey;
|
|
320
|
+
if (!runId || !threadId) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
await this.fetch(this.abortUrl, {
|
|
325
|
+
method: "POST",
|
|
326
|
+
headers: this.headers(),
|
|
327
|
+
body: JSON.stringify({ runId, threadId }),
|
|
328
|
+
});
|
|
329
|
+
} catch {
|
|
330
|
+
// Best-effort: dropping the stream aborts the run server-side anyway.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Answer one approval, on the thread and run it arrived on.
|
|
336
|
+
*
|
|
337
|
+
* `this.active` is deliberately not consulted. An approval pauses a run, and
|
|
338
|
+
* a user is free to open another session from the drawer or start another
|
|
339
|
+
* turn while it waits — at which point `active.sessionKey` is a different
|
|
340
|
+
* conversation and "allow" would be sent against it. The request carried its
|
|
341
|
+
* own `{ runId, sessionKey }`; those are what go on the wire.
|
|
342
|
+
*/
|
|
343
|
+
async resolveApproval(
|
|
344
|
+
id: string,
|
|
345
|
+
decision: AgentApprovalDecision,
|
|
346
|
+
context: AgentApprovalContext,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
const response = await this.fetch(this.approvalUrl, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: this.headers(),
|
|
351
|
+
body: JSON.stringify({
|
|
352
|
+
id,
|
|
353
|
+
decision,
|
|
354
|
+
threadId: context.sessionKey,
|
|
355
|
+
runId: context.runId,
|
|
356
|
+
}),
|
|
357
|
+
});
|
|
358
|
+
// A rejected decision has to reach the user. Swallowing it left the card
|
|
359
|
+
// gone, the run paused server-side and nothing on screen saying so — the
|
|
360
|
+
// user believes they answered and the agent believes nobody did.
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
throw new Error(await readError(response, "approval"));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private emit<T>(sinks: Set<Sink<T>>, event: T): void {
|
|
367
|
+
for (const sink of [...sinks]) {
|
|
368
|
+
sink(event);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* An `AgentChatClient` that streams over AG-UI and reads sessions and history
|
|
375
|
+
* over the REST control plane.
|
|
376
|
+
*
|
|
377
|
+
* ```tsx
|
|
378
|
+
* const client = React.useMemo(
|
|
379
|
+
* () => createAguiAgentChatClient({ baseUrl: "/api/agent", getToken: () => jwt, agentId: "tasks" }),
|
|
380
|
+
* [jwt],
|
|
381
|
+
* );
|
|
382
|
+
* ```
|
|
383
|
+
*/
|
|
384
|
+
export function createAguiAgentChatClient(
|
|
385
|
+
options: CreateAguiAgentChatClientOptions,
|
|
386
|
+
): AgentChatClient {
|
|
387
|
+
return new AguiAgentChatClient(options);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function readError(response: Response, method: string): Promise<string> {
|
|
391
|
+
try {
|
|
392
|
+
const body = (await response.json()) as { error?: { message?: string } };
|
|
393
|
+
if (body?.error?.message) {
|
|
394
|
+
return body.error.message;
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// Not JSON. The status is all there is.
|
|
398
|
+
}
|
|
399
|
+
if (response.status === 404) {
|
|
400
|
+
// The single most common misconfiguration: the control-plane endpoints are
|
|
401
|
+
// off, so every path under /api/core answers 404 before authentication.
|
|
402
|
+
return `${method} is unavailable (404). Check that the AG-UI and control-plane endpoints are enabled.`;
|
|
403
|
+
}
|
|
404
|
+
return `${method} failed (${response.status})`;
|
|
405
|
+
}
|