cortena-ui 1.4.2 → 1.6.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/CHANGELOG.md +107 -0
- package/LICENSE +7 -0
- package/README.md +235 -3
- 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 +353 -0
- package/dist/agent-chat/bridge.js.map +1 -0
- package/dist/agent-chat/session.d.ts +79 -0
- package/dist/agent-chat/session.js +391 -0
- package/dist/agent-chat/session.js.map +1 -0
- package/dist/agent-chat/step-label.d.ts +99 -0
- package/dist/agent-chat/step-label.js +116 -0
- package/dist/agent-chat/step-label.js.map +1 -0
- package/dist/agent-chat/store.d.ts +102 -0
- package/dist/agent-chat/store.js +876 -0
- package/dist/agent-chat/store.js.map +1 -0
- package/dist/agent-chat/types.d.ts +277 -0
- package/dist/agent-chat/types.js +17 -0
- package/dist/agent-chat/types.js.map +1 -0
- package/dist/agent-chat.d.ts +11 -0
- package/dist/agent-chat.js +11 -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 +163 -0
- package/dist/components/agent-chat.js +673 -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 +1 -1
- 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 +18 -5
- package/src/agent-chat/a2ui-block.ts +118 -0
- package/src/agent-chat/agui-client.ts +405 -0
- package/src/agent-chat/bridge.ts +445 -0
- package/src/agent-chat/session.ts +549 -0
- package/src/agent-chat/step-label.ts +177 -0
- package/src/agent-chat/store.ts +1234 -0
- package/src/agent-chat/types.ts +308 -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 +1006 -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 +137 -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,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AG-UI events -> the three `AgentChatClient` sinks.
|
|
3
|
+
*
|
|
4
|
+
* Ported from `cortena-shared/src/agui/bridge.ts` (Cortena monorepo, DESIGN-37
|
|
5
|
+
* step 1). `cortena-shared` is not published, so the translation lives here;
|
|
6
|
+
* keep the two in step. It knows nothing about HTTP or `@ag-ui/client`, which
|
|
7
|
+
* is why the tests can drive it with a literal array of events.
|
|
8
|
+
*
|
|
9
|
+
* One change against the original, and it is the reason this file is not a
|
|
10
|
+
* copy: **TOOL_CALL_ARGS is handled.** cortenacore's mapper emits
|
|
11
|
+
* `TOOL_CALL_START` -> `TOOL_CALL_ARGS { toolCallId, delta }` -> `TOOL_CALL_END`
|
|
12
|
+
* at tool-start time, with the whole argument object stringified into one
|
|
13
|
+
* `delta`. The original bridge had no case for it, so every tool card in the
|
|
14
|
+
* AG-UI path showed a name and a spinner and never showed what the tool was
|
|
15
|
+
* called with. The deltas are accumulated per call id and parsed when they
|
|
16
|
+
* form valid JSON, which is also correct for a transport that streams the
|
|
17
|
+
* arguments a fragment at a time.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { type A2UIChatBlock, foldA2UIBlocks, readA2UIBlock } from "./a2ui-block";
|
|
21
|
+
import type {
|
|
22
|
+
AgentApprovalEvent,
|
|
23
|
+
AgentChatEvent,
|
|
24
|
+
AgentToolEvent,
|
|
25
|
+
AgentApprovalDecision,
|
|
26
|
+
} from "./types";
|
|
27
|
+
|
|
28
|
+
/** The subset of an AG-UI event this bridge reads. */
|
|
29
|
+
export interface AguiIncomingEvent {
|
|
30
|
+
type: string;
|
|
31
|
+
messageId?: string;
|
|
32
|
+
delta?: string;
|
|
33
|
+
toolCallId?: string;
|
|
34
|
+
toolCallName?: string;
|
|
35
|
+
content?: string;
|
|
36
|
+
message?: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
value?: unknown;
|
|
39
|
+
metadata?: Record<string, unknown>;
|
|
40
|
+
snapshot?: unknown;
|
|
41
|
+
outcome?: { type?: string; interrupts?: Array<Record<string, unknown>> };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `CUSTOM` event names cortenacore emits that this bridge acts on. */
|
|
45
|
+
export const AGUI_CUSTOM_NAMES = {
|
|
46
|
+
CHAT_ABORTED: "cortena.chat.aborted",
|
|
47
|
+
TOOL_PROGRESS: "cortena.tool.progress",
|
|
48
|
+
APPROVAL_REQUEST: "cortena.approval.request",
|
|
49
|
+
APPROVAL_RESOLVED: "cortena.approval.resolved",
|
|
50
|
+
A2UI: "cortena.a2ui",
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
export interface AgentChatBridgeOptions {
|
|
54
|
+
runId: string;
|
|
55
|
+
sessionKey: string;
|
|
56
|
+
chat: (event: AgentChatEvent) => void;
|
|
57
|
+
tool?: (event: AgentToolEvent) => void;
|
|
58
|
+
approval?: (event: AgentApprovalEvent) => void;
|
|
59
|
+
now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type ContentBlock = {
|
|
63
|
+
type: "thinking" | "text" | "a2ui";
|
|
64
|
+
thinking?: string;
|
|
65
|
+
text?: string;
|
|
66
|
+
a2ui?: A2UIChatBlock;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const DECISIONS: readonly string[] = ["allow-once", "allow-always", "deny"];
|
|
70
|
+
|
|
71
|
+
export class AgentChatBridge {
|
|
72
|
+
readonly runId: string;
|
|
73
|
+
readonly sessionKey: string;
|
|
74
|
+
|
|
75
|
+
private readonly chat: (event: AgentChatEvent) => void;
|
|
76
|
+
private readonly tool: ((event: AgentToolEvent) => void) | undefined;
|
|
77
|
+
private readonly approval: ((event: AgentApprovalEvent) => void) | undefined;
|
|
78
|
+
private readonly now: () => number;
|
|
79
|
+
|
|
80
|
+
private seq = 0;
|
|
81
|
+
/** Text of the assistant message currently streaming. */
|
|
82
|
+
private text = "";
|
|
83
|
+
private textMessageId: string | null = null;
|
|
84
|
+
private reasoning = "";
|
|
85
|
+
/**
|
|
86
|
+
* The reasoning accumulated before each assistant message began, by message
|
|
87
|
+
* id, and the whole of the re-attach defence for the chain of thought.
|
|
88
|
+
*
|
|
89
|
+
* A re-attach re-streams the run from the top, so every REASONING delta
|
|
90
|
+
* arrives a second time. Restoring this snapshot when a message id starts
|
|
91
|
+
* again rewinds the buffer to exactly the point the first pass reached,
|
|
92
|
+
* whichever side of `TEXT_MESSAGE_START` the reasoning arrived on.
|
|
93
|
+
*/
|
|
94
|
+
private readonly reasoningAtStart = new Map<string, string>();
|
|
95
|
+
/**
|
|
96
|
+
* A2UI blocks seen this run. The chat treats the content array as a
|
|
97
|
+
* snapshot, so every emission carries all of them — the same rule the text
|
|
98
|
+
* follows, and what makes a re-attach land in the same place.
|
|
99
|
+
*/
|
|
100
|
+
private a2ui: A2UIChatBlock[] = [];
|
|
101
|
+
/** Accumulated `TOOL_CALL_ARGS` deltas, per tool call id. */
|
|
102
|
+
private readonly args = new Map<string, string>();
|
|
103
|
+
/**
|
|
104
|
+
* A2UI messages already folded onto each surface, by their serialised form.
|
|
105
|
+
*
|
|
106
|
+
* A re-attach re-streams the run from the beginning, so every
|
|
107
|
+
* `CUSTOM cortena.a2ui` arrives again. Folding concatenates, so without this
|
|
108
|
+
* the surface's message list doubled on the first reconnection and trebled
|
|
109
|
+
* on the second — a table drawn once, then drawn again underneath itself.
|
|
110
|
+
*/
|
|
111
|
+
private readonly seenA2ui = new Map<string, Set<string>>();
|
|
112
|
+
private terminal = false;
|
|
113
|
+
private readonly pendingInterrupts: Array<Record<string, unknown>> = [];
|
|
114
|
+
|
|
115
|
+
constructor(opts: AgentChatBridgeOptions) {
|
|
116
|
+
this.runId = opts.runId;
|
|
117
|
+
this.sessionKey = opts.sessionKey;
|
|
118
|
+
this.chat = opts.chat;
|
|
119
|
+
this.tool = opts.tool;
|
|
120
|
+
this.approval = opts.approval;
|
|
121
|
+
this.now = opts.now ?? (() => Date.now());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** True once RUN_FINISHED or RUN_ERROR has been seen; no retry after this. */
|
|
125
|
+
get finished(): boolean {
|
|
126
|
+
return this.terminal;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Interrupts carried by the terminal RUN_FINISHED, if any. */
|
|
130
|
+
get interrupts(): ReadonlyArray<Record<string, unknown>> {
|
|
131
|
+
return this.pendingInterrupts;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private blocks(): ContentBlock[] {
|
|
135
|
+
const content: ContentBlock[] = [];
|
|
136
|
+
if (this.reasoning) {
|
|
137
|
+
content.push({ type: "thinking", thinking: this.reasoning });
|
|
138
|
+
}
|
|
139
|
+
if (this.text) {
|
|
140
|
+
content.push({ type: "text", text: this.text });
|
|
141
|
+
}
|
|
142
|
+
for (const block of this.a2ui) {
|
|
143
|
+
content.push({ type: "a2ui", a2ui: block });
|
|
144
|
+
}
|
|
145
|
+
return content;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private emitDelta(): void {
|
|
149
|
+
const content = this.blocks();
|
|
150
|
+
if (content.length === 0) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.chat({
|
|
154
|
+
runId: this.runId,
|
|
155
|
+
sessionKey: this.sessionKey,
|
|
156
|
+
seq: ++this.seq,
|
|
157
|
+
state: "delta",
|
|
158
|
+
message: { role: "assistant", content, timestamp: this.now() },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private emitFinal(interrupted = false): void {
|
|
163
|
+
const content = this.blocks();
|
|
164
|
+
this.chat({
|
|
165
|
+
runId: this.runId,
|
|
166
|
+
sessionKey: this.sessionKey,
|
|
167
|
+
seq: ++this.seq,
|
|
168
|
+
state: "final",
|
|
169
|
+
// Carried through so the store can tell "the run succeeded" from "the
|
|
170
|
+
// run is waiting for you". Both arrive as RUN_FINISHED.
|
|
171
|
+
...(interrupted ? { interrupted: true } : {}),
|
|
172
|
+
...(content.length > 0
|
|
173
|
+
? { message: { role: "assistant", content, timestamp: this.now() } }
|
|
174
|
+
: {}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Surface a transport failure the way a run error is surfaced. */
|
|
179
|
+
fail(message: string): void {
|
|
180
|
+
if (this.terminal) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.terminal = true;
|
|
184
|
+
this.chat({
|
|
185
|
+
runId: this.runId,
|
|
186
|
+
sessionKey: this.sessionKey,
|
|
187
|
+
seq: ++this.seq,
|
|
188
|
+
state: "error",
|
|
189
|
+
errorMessage: message,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
handleEvent(event: AguiIncomingEvent): void {
|
|
194
|
+
switch (event.type) {
|
|
195
|
+
case "RUN_STARTED":
|
|
196
|
+
// The run begins here, so the streaming buffers do too. This is the
|
|
197
|
+
// only place the chain of thought is cleared outright.
|
|
198
|
+
this.textMessageId = null;
|
|
199
|
+
this.text = "";
|
|
200
|
+
this.reasoning = "";
|
|
201
|
+
break;
|
|
202
|
+
case "TEXT_MESSAGE_START": {
|
|
203
|
+
// A new assistant message replaces the text buffer, and *only* the
|
|
204
|
+
// text buffer. cortenacore emits the REASONING deltas before the
|
|
205
|
+
// message they explain, so clearing the reasoning here threw away the
|
|
206
|
+
// chain of thought a moment after it arrived — the thinking block
|
|
207
|
+
// never rendered at all. Re-attach doubling is handled instead by
|
|
208
|
+
// rewinding to the snapshot taken when this message id first started.
|
|
209
|
+
const id = event.messageId ?? null;
|
|
210
|
+
this.textMessageId = id;
|
|
211
|
+
this.text = "";
|
|
212
|
+
if (id !== null) {
|
|
213
|
+
const before = this.reasoningAtStart.get(id);
|
|
214
|
+
if (before === undefined) {
|
|
215
|
+
this.reasoningAtStart.set(id, this.reasoning);
|
|
216
|
+
} else {
|
|
217
|
+
this.reasoning = before;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "TEXT_MESSAGE_CONTENT":
|
|
223
|
+
if (event.messageId && event.messageId !== this.textMessageId) {
|
|
224
|
+
this.textMessageId = event.messageId;
|
|
225
|
+
this.text = "";
|
|
226
|
+
}
|
|
227
|
+
this.text += event.delta ?? "";
|
|
228
|
+
this.emitDelta();
|
|
229
|
+
break;
|
|
230
|
+
case "REASONING_MESSAGE_CONTENT":
|
|
231
|
+
this.reasoning += event.delta ?? "";
|
|
232
|
+
this.emitDelta();
|
|
233
|
+
break;
|
|
234
|
+
case "TOOL_CALL_START":
|
|
235
|
+
this.tool?.({
|
|
236
|
+
event: "tool.start",
|
|
237
|
+
data: {
|
|
238
|
+
...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),
|
|
239
|
+
...(event.toolCallName === undefined ? {} : { name: event.toolCallName }),
|
|
240
|
+
startedAt: this.now(),
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
break;
|
|
244
|
+
case "TOOL_CALL_ARGS":
|
|
245
|
+
this.accumulateArgs(event);
|
|
246
|
+
break;
|
|
247
|
+
case "TOOL_CALL_END":
|
|
248
|
+
// Nothing more will arrive for this call's arguments; emit whatever
|
|
249
|
+
// accumulated, even when it never parsed, so the card shows something.
|
|
250
|
+
this.flushArgs(event.toolCallId);
|
|
251
|
+
break;
|
|
252
|
+
case "TOOL_CALL_RESULT":
|
|
253
|
+
// The metadata travels WITH the event. It carries `isError`, and the
|
|
254
|
+
// `meta` a Cortena tool attaches to a failure — the status, the code
|
|
255
|
+
// and the message — and it is the only trustworthy account of how the
|
|
256
|
+
// call went. Dropping it here is what forced the store to guess at an
|
|
257
|
+
// envelope inside `content`, and the guess was wrong for every real
|
|
258
|
+
// result cortenacore emits.
|
|
259
|
+
this.tool?.({
|
|
260
|
+
event: event.metadata?.isError === true ? "tool.error" : "tool.complete",
|
|
261
|
+
data: {
|
|
262
|
+
...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),
|
|
263
|
+
output: event.content,
|
|
264
|
+
...(event.metadata ? { metadata: event.metadata } : {}),
|
|
265
|
+
completedAt: this.now(),
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
break;
|
|
269
|
+
case "CUSTOM":
|
|
270
|
+
this.handleCustom(event);
|
|
271
|
+
break;
|
|
272
|
+
case "RUN_FINISHED": {
|
|
273
|
+
this.terminal = true;
|
|
274
|
+
const interrupted = event.outcome?.type === "interrupt";
|
|
275
|
+
if (interrupted) {
|
|
276
|
+
this.pendingInterrupts.push(...(event.outcome?.interrupts ?? []));
|
|
277
|
+
}
|
|
278
|
+
this.emitFinal(interrupted);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "RUN_ERROR":
|
|
282
|
+
this.terminal = true;
|
|
283
|
+
this.chat({
|
|
284
|
+
runId: this.runId,
|
|
285
|
+
sessionKey: this.sessionKey,
|
|
286
|
+
seq: ++this.seq,
|
|
287
|
+
state: "error",
|
|
288
|
+
errorMessage: event.message ?? "An error occurred",
|
|
289
|
+
});
|
|
290
|
+
break;
|
|
291
|
+
default:
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Accumulate one `TOOL_CALL_ARGS` delta and publish the best reading of it.
|
|
298
|
+
*
|
|
299
|
+
* Published on every delta rather than only at `TOOL_CALL_END`, because a
|
|
300
|
+
* run that is interrupted between the two — an approval, an abort, a dropped
|
|
301
|
+
* stream — would otherwise show a tool card with no input at all, which is
|
|
302
|
+
* the state the card exists to avoid.
|
|
303
|
+
*/
|
|
304
|
+
private accumulateArgs(event: AguiIncomingEvent): void {
|
|
305
|
+
const id = event.toolCallId;
|
|
306
|
+
if (!id) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const next = (this.args.get(id) ?? "") + (event.delta ?? "");
|
|
310
|
+
this.args.set(id, next);
|
|
311
|
+
this.publishArgs(id, next);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private flushArgs(id?: string): void {
|
|
315
|
+
if (!id) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const raw = this.args.get(id);
|
|
319
|
+
if (raw === undefined) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this.args.delete(id);
|
|
323
|
+
this.publishArgs(id, raw);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private publishArgs(id: string, raw: string): void {
|
|
327
|
+
if (!raw) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.tool?.({ event: "tool.args", data: { id, input: parseArgs(raw) } });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The block with any message this surface has already been sent removed;
|
|
335
|
+
* `null` when nothing in it is new.
|
|
336
|
+
*/
|
|
337
|
+
private freshA2UI(block: A2UIChatBlock | null): A2UIChatBlock | null {
|
|
338
|
+
if (!block) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
let seen = this.seenA2ui.get(block.surfaceId);
|
|
342
|
+
if (!seen) {
|
|
343
|
+
seen = new Set();
|
|
344
|
+
this.seenA2ui.set(block.surfaceId, seen);
|
|
345
|
+
}
|
|
346
|
+
const messages: unknown[] = [];
|
|
347
|
+
for (const message of block.messages) {
|
|
348
|
+
const key = stableKey(message);
|
|
349
|
+
if (seen.has(key)) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
seen.add(key);
|
|
353
|
+
messages.push(message);
|
|
354
|
+
}
|
|
355
|
+
// An error still deserves a card even when its messages are all repeats.
|
|
356
|
+
if (messages.length === 0 && !block.error) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
return { ...block, messages };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private handleCustom(event: AguiIncomingEvent): void {
|
|
363
|
+
const value = (event.value ?? {}) as Record<string, unknown>;
|
|
364
|
+
const id = typeof value.id === "string" ? value.id : "";
|
|
365
|
+
switch (event.name) {
|
|
366
|
+
case AGUI_CUSTOM_NAMES.TOOL_PROGRESS:
|
|
367
|
+
this.tool?.({
|
|
368
|
+
event: "tool.progress",
|
|
369
|
+
data: {
|
|
370
|
+
...(typeof value.toolCallId === "string" ? { id: value.toolCallId } : {}),
|
|
371
|
+
output: value.partialResult,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
break;
|
|
375
|
+
case AGUI_CUSTOM_NAMES.APPROVAL_REQUEST: {
|
|
376
|
+
const request = (value.request ?? {}) as Record<string, unknown>;
|
|
377
|
+
this.approval?.({
|
|
378
|
+
type: "request",
|
|
379
|
+
request: {
|
|
380
|
+
id,
|
|
381
|
+
toolName: typeof request.command === "string" ? request.command : "exec",
|
|
382
|
+
args: request,
|
|
383
|
+
timestamp: typeof value.createdAtMs === "number" ? value.createdAtMs : this.now(),
|
|
384
|
+
// Captured here, where the run and the session are facts about the
|
|
385
|
+
// event rather than about whatever is in flight when the user
|
|
386
|
+
// eventually presses a button.
|
|
387
|
+
runId: this.runId,
|
|
388
|
+
sessionKey: this.sessionKey,
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case AGUI_CUSTOM_NAMES.A2UI: {
|
|
394
|
+
// One event per block; folding here means two pushes to one surface
|
|
395
|
+
// reach the renderer as a single message list, which is how a surface
|
|
396
|
+
// the agent streams in pieces ends up drawn once.
|
|
397
|
+
const block = this.freshA2UI(readA2UIBlock(value));
|
|
398
|
+
if (block) {
|
|
399
|
+
this.a2ui = foldA2UIBlocks([...this.a2ui, block]);
|
|
400
|
+
this.emitDelta();
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case AGUI_CUSTOM_NAMES.APPROVAL_RESOLVED:
|
|
405
|
+
this.approval?.({
|
|
406
|
+
type: "resolved",
|
|
407
|
+
id,
|
|
408
|
+
decision:
|
|
409
|
+
typeof value.decision === "string" && DECISIONS.includes(value.decision)
|
|
410
|
+
? (value.decision as AgentApprovalDecision)
|
|
411
|
+
: null,
|
|
412
|
+
});
|
|
413
|
+
break;
|
|
414
|
+
default:
|
|
415
|
+
// cortena.chat.aborted needs no change here: abort() has already reset
|
|
416
|
+
// the local streaming state before the request went out.
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* A message's identity for de-duplication.
|
|
424
|
+
*
|
|
425
|
+
* `JSON.stringify` is enough: these are payloads the transport just parsed out
|
|
426
|
+
* of JSON, so key order is the order the server wrote, and the same message
|
|
427
|
+
* arriving twice on a re-attach serialises identically both times.
|
|
428
|
+
*/
|
|
429
|
+
function stableKey(message: unknown): string {
|
|
430
|
+
try {
|
|
431
|
+
return JSON.stringify(message) ?? "undefined";
|
|
432
|
+
} catch {
|
|
433
|
+
// Circular, which a JSON transport cannot produce. Never equal to anything.
|
|
434
|
+
return `${Math.random()}`;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Arguments as an object when the accumulation parses, as the raw text otherwise. */
|
|
439
|
+
function parseArgs(raw: string): unknown {
|
|
440
|
+
try {
|
|
441
|
+
return JSON.parse(raw);
|
|
442
|
+
} catch {
|
|
443
|
+
return raw;
|
|
444
|
+
}
|
|
445
|
+
}
|