@artooi/ag-ui-web-component 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -36
- package/README.md +7 -7
- package/dist/ag-ui-web-component.bundle.js +145 -118
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +65 -78
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +93 -126
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +24 -30
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +9 -14
- package/dist/core/attachment.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +20 -27
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +13 -15
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +8 -9
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/run_index.d.ts +11 -20
- package/dist/core/run_index.d.ts.map +1 -1
- package/dist/core/transcribe_audio.d.ts +8 -8
- package/dist/core/transcribe_audio.d.ts.map +1 -1
- package/dist/core/upload_attachment.d.ts +15 -18
- package/dist/core/upload_attachment.d.ts.map +1 -1
- package/dist/core/utils.d.ts +4 -6
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/dom/animations.d.ts +22 -30
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/dom_driver.d.ts +7 -7
- package/dist/dom/native_setter.d.ts +2 -2
- package/dist/dom/native_setter.d.ts.map +1 -1
- package/dist/index.js +347 -401
- package/dist/index.js.map +2 -2
- package/dist/skills/fill_template.d.ts +4 -5
- package/dist/skills/fill_template.d.ts.map +1 -1
- package/dist/skills/parse_skills.d.ts.map +1 -1
- package/dist/skills/skill.d.ts +7 -8
- package/dist/skills/skill.d.ts.map +1 -1
- package/dist/tools/client_tool_registry.d.ts +2 -2
- package/dist/tools/page_action_tools.d.ts +7 -10
- package/dist/tools/page_action_tools.d.ts.map +1 -1
- package/dist/tools/page_state.d.ts +5 -8
- package/dist/tools/page_state.d.ts.map +1 -1
- package/dist/tools/route_map.d.ts +7 -10
- package/dist/tools/route_map.d.ts.map +1 -1
- package/dist/ui/approval_card.d.ts +15 -20
- package/dist/ui/approval_card.d.ts.map +1 -1
- package/dist/ui/attach_copy_buttons.d.ts +4 -10
- package/dist/ui/attach_copy_buttons.d.ts.map +1 -1
- package/dist/ui/attachment_chips.d.ts +13 -5
- package/dist/ui/attachment_chips.d.ts.map +1 -1
- package/dist/ui/attachment_tray.d.ts +6 -6
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- package/dist/ui/checkpoint_menu.d.ts +7 -8
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/confirmation_card.d.ts +10 -15
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/question_card.d.ts +12 -15
- package/dist/ui/question_card.d.ts.map +1 -1
- package/dist/ui/relative_time.d.ts +5 -7
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/render_markdown.d.ts +8 -8
- package/dist/ui/render_markdown.d.ts.map +1 -1
- package/dist/ui/resize_handle.d.ts +21 -34
- package/dist/ui/resize_handle.d.ts.map +1 -1
- package/dist/ui/run_notice.d.ts +5 -7
- package/dist/ui/run_notice.d.ts.map +1 -1
- package/dist/ui/skills_menu.d.ts +4 -5
- package/dist/ui/skills_menu.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/thoughts_block.d.ts +9 -11
- package/dist/ui/thoughts_block.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts +6 -5
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +17 -25
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +6 -12
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts +10 -11
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +71 -80
- package/src/core/ag_ui_chat.ts +242 -292
- package/src/core/agui_client.ts +60 -71
- package/src/core/attachment.ts +9 -14
- package/src/core/conversation_store.ts +25 -33
- package/src/core/create_http_agent.ts +18 -22
- package/src/core/remote_conversation_store.ts +14 -15
- package/src/core/run_index.ts +14 -23
- package/src/core/transcribe_audio.ts +9 -10
- package/src/core/upload_attachment.ts +18 -21
- package/src/core/utils.ts +4 -6
- package/src/dom/animations.ts +33 -43
- package/src/dom/dom_driver.ts +7 -7
- package/src/dom/native_setter.ts +11 -12
- package/src/skills/fill_template.ts +4 -5
- package/src/skills/parse_skills.ts +3 -4
- package/src/skills/skill.ts +7 -8
- package/src/tools/client_tool_registry.ts +2 -2
- package/src/tools/page_action_tools.ts +12 -15
- package/src/tools/page_state.ts +5 -8
- package/src/tools/route_map.ts +15 -19
- package/src/ui/approval_card.ts +15 -20
- package/src/ui/attach_copy_buttons.ts +9 -18
- package/src/ui/attachment_chips.ts +19 -10
- package/src/ui/attachment_tray.ts +8 -7
- package/src/ui/checkpoint_menu.ts +7 -8
- package/src/ui/confirmation_card.ts +10 -15
- package/src/ui/question_card.ts +12 -15
- package/src/ui/relative_time.ts +5 -7
- package/src/ui/render_markdown.ts +25 -51
- package/src/ui/resize_handle.ts +25 -38
- package/src/ui/run_notice.ts +9 -12
- package/src/ui/skills_menu.ts +4 -5
- package/src/ui/styles.ts +122 -93
- package/src/ui/thoughts_block.ts +11 -13
- package/src/ui/thread_drawer.ts +6 -5
- package/src/ui/tool_call_card.ts +22 -32
- package/src/ui/ui_strings.ts +6 -12
- package/src/ui/voice_input.ts +10 -11
- package/src/version.ts +1 -1
package/src/core/agui_client.ts
CHANGED
|
@@ -33,9 +33,9 @@ export interface ToolExecution {
|
|
|
33
33
|
/**
|
|
34
34
|
* Executes a frontend tool call.
|
|
35
35
|
*
|
|
36
|
-
* Returns the {@link ToolExecution} to post back to the agent, or
|
|
37
|
-
* the call is not a frontend tool the host owns
|
|
38
|
-
* already executed
|
|
36
|
+
* Returns the {@link ToolExecution} to post back to the agent, or `null` when
|
|
37
|
+
* the call is not a frontend tool the host owns — a server-side tool the server
|
|
38
|
+
* already executed, which the client must not re-run.
|
|
39
39
|
*/
|
|
40
40
|
export type ExecuteTool = (call: AgUiToolCall) => Promise<ToolExecution | null>;
|
|
41
41
|
|
|
@@ -50,11 +50,10 @@ export type InterruptResponse = { status: "resolved"; payload?: unknown } | { st
|
|
|
50
50
|
/**
|
|
51
51
|
* Resolves the approval interrupts a run finished on, keyed by interrupt id.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* unresolved interrupt then simply ends the loop.
|
|
53
|
+
* A gated server-side tool defers instead of executing, so the run finishes on
|
|
54
|
+
* an interrupt outcome; the host collects one decision per interrupt and the
|
|
55
|
+
* loop resumes with the answers. Omit for agents that never gate server-side
|
|
56
|
+
* tools — an unresolved interrupt then ends the loop.
|
|
58
57
|
*/
|
|
59
58
|
export type ResolveInterrupts = (
|
|
60
59
|
interrupts: readonly Interrupt[],
|
|
@@ -67,9 +66,9 @@ export type ResolveInterrupts = (
|
|
|
67
66
|
*/
|
|
68
67
|
export interface AgUiClientHandlers {
|
|
69
68
|
onRunStart(): void;
|
|
70
|
-
/** Fired on every streamed token;
|
|
69
|
+
/** Fired on every streamed token; `buffer` is the full text so far. */
|
|
71
70
|
onTextDelta(buffer: string): void;
|
|
72
|
-
/** Fired when the assistant message completes;
|
|
71
|
+
/** Fired when the assistant message completes; `buffer` is the final text. */
|
|
73
72
|
onTextEnd(buffer: string): void;
|
|
74
73
|
/** Fired when the agent finishes calling a tool (server- or frontend-side). */
|
|
75
74
|
onToolCall(call: AgUiToolCall): void;
|
|
@@ -95,17 +94,15 @@ export interface AgUiClientHandlers {
|
|
|
95
94
|
onError(message: string): void;
|
|
96
95
|
/**
|
|
97
96
|
* Fired when the user cancelled the run ({@link AgUiClient.cancel}) — the
|
|
98
|
-
* deliberate-stop sibling of `onError`.
|
|
99
|
-
* streamed stays valid
|
|
100
|
-
*
|
|
97
|
+
* deliberate-stop sibling of `onError`. Partial assistant text already
|
|
98
|
+
* streamed stays valid, so the host keeps the bubble and shows a stopped
|
|
99
|
+
* affordance rather than an error. `onSettled` still follows.
|
|
101
100
|
*/
|
|
102
101
|
onCancelled(): void;
|
|
103
102
|
/**
|
|
104
|
-
* Fired exactly once when the
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* to rest (pending indicator cleared, input re-enabled) no matter how the
|
|
108
|
-
* run finished.
|
|
103
|
+
* Fired exactly once when the interaction settles, however the run loop ended
|
|
104
|
+
* — server-only round, rounds exhausted, cancellation, or error. The terminal
|
|
105
|
+
* guarantee that the UI returns to rest.
|
|
109
106
|
*/
|
|
110
107
|
onSettled(): void;
|
|
111
108
|
}
|
|
@@ -138,17 +135,15 @@ export interface AgUiClientConfig extends AgUiRunInputs {
|
|
|
138
135
|
*/
|
|
139
136
|
onPersist?: (messages: readonly Message[]) => void;
|
|
140
137
|
/**
|
|
141
|
-
* Called whenever AG-UI shared state changes —
|
|
142
|
-
* `
|
|
143
|
-
*
|
|
144
|
-
* the result so a host can react.
|
|
138
|
+
* Called whenever AG-UI shared state changes — a streamed `STATE_SNAPSHOT` /
|
|
139
|
+
* `STATE_DELTA`, or {@link AgUiClient.setState}. `@ag-ui/client` owns applying
|
|
140
|
+
* those events; this only forwards the result.
|
|
145
141
|
*/
|
|
146
142
|
onStateChanged?: (state: Readonly<Record<string, unknown>>) => void;
|
|
147
143
|
/**
|
|
148
|
-
* Error text
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* localized string.
|
|
144
|
+
* Error text for {@link AgUiClientHandlers.onError} when a run's stream closes
|
|
145
|
+
* without a terminal AG-UI event. Defaults to `"Connection lost"`; the host
|
|
146
|
+
* passes its localized string.
|
|
152
147
|
*/
|
|
153
148
|
connectionLostMessage?: string;
|
|
154
149
|
}
|
|
@@ -182,10 +177,9 @@ export class AgUiClient {
|
|
|
182
177
|
readonly #resolveInterrupts: ResolveInterrupts | null;
|
|
183
178
|
readonly #onPersist: (messages: readonly Message[]) => void;
|
|
184
179
|
/**
|
|
185
|
-
* Message ids the server has already closed, so a reuse can be reported.
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* case a per-run set would miss entirely.
|
|
180
|
+
* Message ids the server has already closed, so a reuse can be reported. Per
|
|
181
|
+
* client rather than per run — the merge happens across runs, which a per-run
|
|
182
|
+
* set would miss entirely.
|
|
189
183
|
*/
|
|
190
184
|
readonly #closedMessageIds = new Set<string>();
|
|
191
185
|
readonly #connectionLostMessage: string;
|
|
@@ -205,8 +199,8 @@ export class AgUiClient {
|
|
|
205
199
|
const onStateChanged = config.onStateChanged;
|
|
206
200
|
if (onStateChanged !== undefined) {
|
|
207
201
|
// The agent applies STATE_SNAPSHOT / STATE_DELTA itself; subscribing is
|
|
208
|
-
// how
|
|
209
|
-
//
|
|
202
|
+
// how the result is learned rather than re-derived from the event stream.
|
|
203
|
+
// Lives for the agent's lifetime, which is this client's.
|
|
210
204
|
this.#agent.subscribe({
|
|
211
205
|
onStateChanged: ({ state }) => {
|
|
212
206
|
onStateChanged(state as Record<string, unknown>);
|
|
@@ -243,9 +237,8 @@ export class AgUiClient {
|
|
|
243
237
|
* tools (bounded by {@link MAX_TOOL_ROUNDS}).
|
|
244
238
|
*
|
|
245
239
|
* `attachments` ride on the user message as a non-standard field so the
|
|
246
|
-
* default
|
|
247
|
-
*
|
|
248
|
-
* unknown message field), then reads bytes via the `read_attachment` tool.
|
|
240
|
+
* default store round-trips them for history replay; see
|
|
241
|
+
* {@link messageAttachments}.
|
|
249
242
|
*/
|
|
250
243
|
async send(content: string, attachments: readonly AttachmentRef[] = []): Promise<void> {
|
|
251
244
|
// Cast at the AG-UI boundary: `attachments` is a web-component augmentation
|
|
@@ -263,7 +256,7 @@ export class AgUiClient {
|
|
|
263
256
|
/**
|
|
264
257
|
* Resume the run loop after a navigating tool's result was supplied
|
|
265
258
|
* post-reload (via {@link addToolResult}). Unlike {@link send}, adds no user
|
|
266
|
-
* message
|
|
259
|
+
* message; it continues the conversation already in history.
|
|
267
260
|
*/
|
|
268
261
|
async resume(): Promise<void> {
|
|
269
262
|
await this.#run();
|
|
@@ -309,21 +302,21 @@ export class AgUiClient {
|
|
|
309
302
|
}
|
|
310
303
|
|
|
311
304
|
#onCancelled(): void {
|
|
312
|
-
//
|
|
313
|
-
//
|
|
305
|
+
// Persist so the truncated exchange, partial assistant text included,
|
|
306
|
+
// survives a reload.
|
|
314
307
|
this.#onPersist(this.#agent.messages);
|
|
315
308
|
this.#handlers.onCancelled();
|
|
316
309
|
}
|
|
317
310
|
|
|
318
311
|
async #runLoop(): Promise<void> {
|
|
319
|
-
// Carries
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
312
|
+
// Carries resolved approval answers into the next run when a round finished
|
|
313
|
+
// on a server-side-tool interrupt. Distinct from the public resume(), which
|
|
314
|
+
// continues an unfinished frontend-tool round after a page load; this stays
|
|
315
|
+
// inside one #run().
|
|
323
316
|
let resume: ResumeEntry[] | undefined;
|
|
324
317
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
325
|
-
// A cancel during the previous round's
|
|
326
|
-
//
|
|
318
|
+
// A cancel during the previous round's tool execution lands here: the
|
|
319
|
+
// running handler completed, but no further round starts.
|
|
327
320
|
if (this.#cancelled) {
|
|
328
321
|
return;
|
|
329
322
|
}
|
|
@@ -339,29 +332,28 @@ export class AgUiClient {
|
|
|
339
332
|
await this.#agent.runAgent(params, this.#buildSubscriber(pending, runState));
|
|
340
333
|
resume = undefined;
|
|
341
334
|
this.#onPersist(this.#agent.messages);
|
|
342
|
-
// Cancelled mid-stream:
|
|
343
|
-
//
|
|
335
|
+
// Cancelled mid-stream: don't execute the tool calls collected before the
|
|
336
|
+
// abort.
|
|
344
337
|
if (this.#cancelled) {
|
|
345
338
|
return;
|
|
346
339
|
}
|
|
347
|
-
// The stream resolved without RUN_FINISHED / RUN_ERROR
|
|
348
|
-
// dropped mid-run. Surface it as an error
|
|
349
|
-
// with a stuck pending indicator
|
|
340
|
+
// The stream resolved without RUN_FINISHED / RUN_ERROR, so the transport
|
|
341
|
+
// dropped mid-run. Surface it as an error (via #run) rather than resting
|
|
342
|
+
// silently with a stuck pending indicator.
|
|
350
343
|
if (!runState.terminal) {
|
|
351
344
|
throw new ConnectionLostError(this.#connectionLostMessage);
|
|
352
345
|
}
|
|
353
|
-
// RUN_ERROR is terminal
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
346
|
+
// RUN_ERROR is terminal and the agent already reported it via onError.
|
|
347
|
+
// Executing the calls collected before it, or starting another round,
|
|
348
|
+
// would run into a broken context and raise a confusing second error.
|
|
349
|
+
// Pending tool cards are swept at onSettled.
|
|
357
350
|
if (runState.errored) {
|
|
358
351
|
return;
|
|
359
352
|
}
|
|
360
|
-
// A gated server-side tool deferred
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
// over the frontend-tool sweep below: a server-side tool isn't ours to run.
|
|
353
|
+
// A gated server-side tool deferred, so the run finished on an interrupt
|
|
354
|
+
// outcome: resolve each interrupt, then re-enter carrying the answers so
|
|
355
|
+
// the follow-up run executes or denies the tool. Must precede the
|
|
356
|
+
// frontend-tool sweep below — a server-side tool isn't ours to run.
|
|
365
357
|
if (runState.interrupts.length > 0) {
|
|
366
358
|
if (this.#resolveInterrupts === null) {
|
|
367
359
|
return;
|
|
@@ -410,13 +402,10 @@ export class AgUiClient {
|
|
|
410
402
|
h.onRunStart();
|
|
411
403
|
},
|
|
412
404
|
onTextMessageStartEvent({ event }) {
|
|
413
|
-
// A
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
// but it should not be silent, because the corruption outlives the
|
|
418
|
-
// session and reads as a client bug. Found by a demo harness doing
|
|
419
|
-
// exactly this.
|
|
405
|
+
// A reused message id merges two answers into one persisted transcript
|
|
406
|
+
// entry. The protocol has no rule to enforce and refusing the event
|
|
407
|
+
// would be worse than the merge, so warn and continue — but do not stay
|
|
408
|
+
// silent, since the corruption outlives the session.
|
|
420
409
|
if (closed.has(event.messageId)) {
|
|
421
410
|
console.warn(
|
|
422
411
|
`<ag-ui-chat>: the server reused message id "${event.messageId}", which was ` +
|
|
@@ -448,9 +437,9 @@ export class AgUiClient {
|
|
|
448
437
|
onActivitySnapshotEvent({ event }) {
|
|
449
438
|
h.onActivity(event.activityType, event.content);
|
|
450
439
|
},
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
440
|
+
// `@ag-ui/client` maps the deprecated THINKING_* events onto these
|
|
441
|
+
// REASONING_* callbacks, so the reasoning family alone covers both
|
|
442
|
+
// protocol versions.
|
|
454
443
|
onReasoningStartEvent() {
|
|
455
444
|
h.onReasoningStart();
|
|
456
445
|
},
|
|
@@ -462,9 +451,9 @@ export class AgUiClient {
|
|
|
462
451
|
},
|
|
463
452
|
onRunFinishedEvent(params) {
|
|
464
453
|
// RUN_FINISHED is terminal for both a normal finish and an interrupt.
|
|
465
|
-
//
|
|
466
|
-
// `pendingInterrupts`
|
|
467
|
-
//
|
|
454
|
+
// Capture the interrupts here rather than reading the agent's
|
|
455
|
+
// `pendingInterrupts` afterwards, to stay independent of that field's
|
|
456
|
+
// cross-run clearing semantics.
|
|
468
457
|
runState.terminal = true;
|
|
469
458
|
if (params.outcome === "interrupt") {
|
|
470
459
|
runState.interrupts = params.interrupts;
|
package/src/core/attachment.ts
CHANGED
|
@@ -4,11 +4,9 @@ import type { Message } from "@ag-ui/core";
|
|
|
4
4
|
* A durable, lightweight reference to one uploaded file — what an upload
|
|
5
5
|
* returns and what rides on a sent message, never the bytes.
|
|
6
6
|
*
|
|
7
|
-
* Mirrors django-ag-ui's `AttachmentRef`:
|
|
7
|
+
* Mirrors django-ag-ui's `AttachmentRef`: the file uploads out-of-band to the
|
|
8
8
|
* attachments endpoint, the server hands back this ref, and the agent reads the
|
|
9
|
-
*
|
|
10
|
-
* message stream free of base64 mirrors how the tool catalog keeps schemas off
|
|
11
|
-
* the wire.
|
|
9
|
+
* content server-side via the `read_attachment` tool.
|
|
12
10
|
*/
|
|
13
11
|
export interface AttachmentRef {
|
|
14
12
|
/** Opaque, owner-scoped handle the server resolves back to bytes. */
|
|
@@ -26,17 +24,14 @@ export interface AttachmentRef {
|
|
|
26
24
|
/**
|
|
27
25
|
* The attachment refs a user message carries.
|
|
28
26
|
*
|
|
29
|
-
* Refs
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `RunAgentInput` validation ignores the unknown field — the model learns the
|
|
34
|
-
* ids from the run context manifest instead.
|
|
27
|
+
* Refs ride on the message as a non-standard `attachments` field, which the
|
|
28
|
+
* default store round-trips and `@ag-ui/client` preserves through `addMessage`
|
|
29
|
+
* / `structuredClone`, so a restored conversation re-renders its chips. The
|
|
30
|
+
* server's strict `RunAgentInput` validation ignores the unknown field.
|
|
35
31
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* abort the whole history replay.
|
|
32
|
+
* Storage is untrusted — hand-edited, truncated, or corrupted — so malformed
|
|
33
|
+
* entries are dropped here; a shapeless one would throw in `iconFor` and abort
|
|
34
|
+
* the whole history replay.
|
|
40
35
|
*/
|
|
41
36
|
export function messageAttachments(message: Message): readonly AttachmentRef[] {
|
|
42
37
|
const refs = (message as { attachments?: unknown }).attachments;
|
|
@@ -13,13 +13,11 @@ export interface NavigationCheckpoint {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Lightweight metadata for one conversation — the thread-drawer row shape
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* is a one-line excerpt of the latest message; `updatedAt` is epoch ms of the
|
|
22
|
-
* last change, used to order the list.
|
|
16
|
+
* Lightweight metadata for one conversation — the thread-drawer row shape, so
|
|
17
|
+
* the drawer renders without loading message bodies. `title` defaults to a
|
|
18
|
+
* truncation of the first user message until an explicit rename, `preview` is a
|
|
19
|
+
* one-line excerpt of the latest message, and `updatedAt` is epoch ms of the
|
|
20
|
+
* last change, which orders the list.
|
|
23
21
|
*/
|
|
24
22
|
export interface ThreadMeta {
|
|
25
23
|
readonly threadId: string;
|
|
@@ -32,16 +30,14 @@ export interface ThreadMeta {
|
|
|
32
30
|
* Client-side persistence seam for the conversation and a pending-navigation
|
|
33
31
|
* checkpoint, keyed by `thread_id`.
|
|
34
32
|
*
|
|
35
|
-
* The default {@link SessionStorageStore} keeps everything per-tab
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* are therefore async-friendly. The checkpoint methods stay synchronous — the
|
|
40
|
-
* marker is a tiny local hint a server store can derive from history and no-op.
|
|
33
|
+
* The default {@link SessionStorageStore} keeps everything per-tab. A host may
|
|
34
|
+
* inject a server-backed store instead, which is why `loadMessages` and
|
|
35
|
+
* `listThreads` are async. The checkpoint methods stay synchronous: the marker
|
|
36
|
+
* is a small local hint a server store can derive from history and no-op.
|
|
41
37
|
*
|
|
42
|
-
* Thread enumeration
|
|
43
|
-
*
|
|
44
|
-
*
|
|
38
|
+
* Thread enumeration backs the chat-history drawer; deleting a thread reuses
|
|
39
|
+
* {@link clear} and "new chat" reuses {@link threadId} after clearing the
|
|
40
|
+
* active thread.
|
|
45
41
|
*/
|
|
46
42
|
export interface ClientConversationStore {
|
|
47
43
|
/** The active conversation id, generated and persisted on first read. */
|
|
@@ -86,18 +82,15 @@ interface StoredThread {
|
|
|
86
82
|
/**
|
|
87
83
|
* Default {@link ClientConversationStore}: per-tab `sessionStorage`.
|
|
88
84
|
*
|
|
89
|
-
* Survives full page reloads and same-tab navigation, clears on tab close
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* the drawer so it works with no server.
|
|
85
|
+
* Survives full page reloads and same-tab navigation, clears on tab close.
|
|
86
|
+
* Tracks multiple threads per tab: the active id under one key, message history
|
|
87
|
+
* and checkpoint namespaced by id, and a small index feeding the drawer with no
|
|
88
|
+
* server involved.
|
|
94
89
|
*
|
|
95
|
-
* An optional `namespace` scopes every key to one element
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* (`ag-ui-chat:*`) keys into it once, so an existing conversation survives the
|
|
100
|
-
* upgrade; the default empty namespace keeps the legacy origin-global keys.
|
|
90
|
+
* An optional `namespace` scopes every key to one element, so two
|
|
91
|
+
* `<ag-ui-chat>` instances on the same origin keep separate active-thread
|
|
92
|
+
* pointers and drawer indexes instead of clobbering each other. The default
|
|
93
|
+
* empty namespace keeps the origin-global keys; see {@link #migrateLegacyKeys}.
|
|
101
94
|
*/
|
|
102
95
|
export class SessionStorageStore implements ClientConversationStore {
|
|
103
96
|
readonly #root: string;
|
|
@@ -219,12 +212,11 @@ export class SessionStorageStore implements ClientConversationStore {
|
|
|
219
212
|
}
|
|
220
213
|
|
|
221
214
|
/**
|
|
222
|
-
* One-time move of
|
|
223
|
-
* namespace, so an existing conversation isn't orphaned
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* second namespace finds it gone and starts fresh.
|
|
215
|
+
* One-time move of un-namespaced `ag-ui-chat:*` keys into this instance's
|
|
216
|
+
* namespace, so an existing conversation isn't orphaned. Only this store's own
|
|
217
|
+
* keys move — the element's `collapsed` / `theme` keys are left alone. The
|
|
218
|
+
* first namespaced instance to mount adopts the data; a second namespace
|
|
219
|
+
* finds it gone and starts fresh.
|
|
228
220
|
*/
|
|
229
221
|
#migrateLegacyKeys(): void {
|
|
230
222
|
const legacyRoot = `${KEY_ROOT}:`;
|
|
@@ -8,20 +8,18 @@ export interface HttpAgentOptions {
|
|
|
8
8
|
headers?: Record<string, string>;
|
|
9
9
|
/**
|
|
10
10
|
* Cookie policy for the run request, as `fetch`'s own `credentials` mode.
|
|
11
|
-
* Unset leaves the browser default (`same-origin`)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Unset leaves the browser default (`same-origin`), which sends no cookies
|
|
12
|
+
* to an agent endpoint on a different origin or subdomain. A
|
|
13
|
+
* cookie-authenticated cross-origin deployment needs `"include"` here plus a
|
|
14
|
+
* server answering with `Access-Control-Allow-Credentials: true` and a
|
|
15
|
+
* concrete origin.
|
|
16
16
|
*/
|
|
17
17
|
credentials?: RequestCredentials;
|
|
18
18
|
/**
|
|
19
|
-
* Live header source, re-read on
|
|
20
|
-
* static `headers`
|
|
21
|
-
* for the whole conversation
|
|
22
|
-
*
|
|
23
|
-
* mid-conversation. When set, the fetch wrapper overlays these values on
|
|
24
|
-
* each call; `headers` still seeds the initial/static configuration.
|
|
19
|
+
* Live header source, re-read on every request, overlaid on `headers`.
|
|
20
|
+
* `HttpAgent` bakes static `headers` in at construction and the element
|
|
21
|
+
* caches the agent for the whole conversation, so rotating credentials (CSRF,
|
|
22
|
+
* short-lived JWT) must come through here.
|
|
25
23
|
*/
|
|
26
24
|
getHeaders?: () => Record<string, string>;
|
|
27
25
|
/** Stable conversation id, so the agent's runs share a thread. */
|
|
@@ -37,11 +35,11 @@ export interface HttpAgentOptions {
|
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
/**
|
|
40
|
-
* Build an AG-UI {@link HttpAgent} pointed at
|
|
38
|
+
* Build an AG-UI {@link HttpAgent} pointed at `endpoint`.
|
|
41
39
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* The default agent factory for `<ag-ui-chat>`; tests and advanced hosts
|
|
41
|
+
* override the element's `agentFactory` to inject a different
|
|
42
|
+
* {@link AbstractAgent}.
|
|
45
43
|
*/
|
|
46
44
|
export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
|
|
47
45
|
return new HttpAgent({
|
|
@@ -49,12 +47,10 @@ export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
|
|
|
49
47
|
headers: options.headers ?? {},
|
|
50
48
|
initialState: { ...(options.initialState ?? {}) },
|
|
51
49
|
// HttpAgent invokes its configured fetch as a method (`this.fetch(...)`),
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// reaches the stream even though the agent instance is cached, and applies
|
|
57
|
-
// the configured cookie policy — the agent's own config has no seam for it.
|
|
50
|
+
// rebinding the global `fetch` to the agent instance — "Illegal invocation"
|
|
51
|
+
// in browsers. The wrapper keeps `fetch` a free call, and is also where the
|
|
52
|
+
// per-request `getHeaders()` overlay and the cookie policy go, the agent's
|
|
53
|
+
// own config having no seam for either.
|
|
58
54
|
fetch: (url, init) => {
|
|
59
55
|
const fresh = options.getHeaders?.();
|
|
60
56
|
if (fresh === undefined) {
|
|
@@ -67,7 +63,7 @@ export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
|
|
|
67
63
|
return fetch(url, withCredentials({ ...init, headers }, options.credentials));
|
|
68
64
|
},
|
|
69
65
|
// Spread conditionally: under `exactOptionalPropertyTypes` an explicit
|
|
70
|
-
// `undefined` is not assignable to
|
|
66
|
+
// `undefined` is not assignable to an optional field.
|
|
71
67
|
...(options.threadId !== undefined ? { threadId: options.threadId } : {}),
|
|
72
68
|
...(options.initialMessages !== undefined
|
|
73
69
|
? { initialMessages: [...options.initialMessages] }
|
|
@@ -19,10 +19,9 @@ interface ServerThreadRow {
|
|
|
19
19
|
type HeadersProvider = () => Record<string, string>;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Live cookie policy, read per request. A provider rather than a value
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* during that first frame.
|
|
22
|
+
* Live cookie policy, read per request. A provider rather than a value: the
|
|
23
|
+
* store is built once on connect, but a host may configure the element after
|
|
24
|
+
* inserting it, and a captured value would pin that first frame's setting.
|
|
26
25
|
*/
|
|
27
26
|
type CredentialsProvider = () => RequestCredentials | undefined;
|
|
28
27
|
|
|
@@ -36,11 +35,11 @@ type CredentialsProvider = () => RequestCredentials | undefined;
|
|
|
36
35
|
* - `PATCH <url><id>/` → rename (`{ "title": … }`);
|
|
37
36
|
* - `DELETE <url><id>/` → delete.
|
|
38
37
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
38
|
+
* Wraps a local store (default {@link SessionStorageStore}) for the client-only
|
|
39
|
+
* concerns — active thread id, navigation checkpoint, message cache — and as
|
|
40
|
+
* the fallback when a request fails. Rename and delete apply optimistically via
|
|
41
|
+
* a local overlay, so the drawer reflects them before the fire-and-forget
|
|
42
|
+
* round-trip lands.
|
|
44
43
|
*/
|
|
45
44
|
export class RemoteConversationStore implements ClientConversationStore {
|
|
46
45
|
readonly #url: string;
|
|
@@ -108,9 +107,9 @@ export class RemoteConversationStore implements ClientConversationStore {
|
|
|
108
107
|
if (response === null || !response.ok) {
|
|
109
108
|
return this.#local.loadMessages(threadId);
|
|
110
109
|
}
|
|
111
|
-
// A 200 whose body isn't
|
|
112
|
-
//
|
|
113
|
-
// `void #rehydrate()` swallows
|
|
110
|
+
// A 200 whose body isn't JSON (a proxy's HTML error page, a truncated
|
|
111
|
+
// stream) must not throw an unhandled rejection that the caller's
|
|
112
|
+
// `void #rehydrate()` swallows; fall back to the local cache.
|
|
114
113
|
const body = await this.#readJson<{ messages?: readonly Message[] }>(response);
|
|
115
114
|
if (body === null) {
|
|
116
115
|
return this.#local.loadMessages(threadId);
|
|
@@ -143,9 +142,9 @@ export class RemoteConversationStore implements ClientConversationStore {
|
|
|
143
142
|
return {
|
|
144
143
|
threadId: row.thread_id,
|
|
145
144
|
title: this.#renamed.get(row.thread_id) ?? row.title,
|
|
146
|
-
// `null`
|
|
147
|
-
// signal
|
|
148
|
-
//
|
|
145
|
+
// `null` and an unparseable date both become NaN, Date.parse's own
|
|
146
|
+
// signal, which `relativeTime` renders as a neutral label rather than an
|
|
147
|
+
// epoch-0 or NaN duration.
|
|
149
148
|
updatedAt: row.updated_at === null ? Number.NaN : Date.parse(row.updated_at),
|
|
150
149
|
preview: row.preview,
|
|
151
150
|
};
|
package/src/core/run_index.ts
CHANGED
|
@@ -13,12 +13,7 @@ export interface RunRow {
|
|
|
13
13
|
/** Live header source, read per request so rotated tokens / CSRF reach the server. */
|
|
14
14
|
type HeadersProvider = () => Record<string, string>;
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Live cookie policy, read per request. A provider rather than a value because
|
|
18
|
-
* the index is built once (on connect) and kept, while a host may configure the
|
|
19
|
-
* element after inserting it — a captured value would pin whatever was set
|
|
20
|
-
* during that first frame.
|
|
21
|
-
*/
|
|
16
|
+
/** Live cookie policy, read per request; see `RemoteConversationStore`'s note. */
|
|
22
17
|
type CredentialsProvider = () => RequestCredentials | undefined;
|
|
23
18
|
|
|
24
19
|
/**
|
|
@@ -29,17 +24,14 @@ type CredentialsProvider = () => RequestCredentials | undefined;
|
|
|
29
24
|
*
|
|
30
25
|
* - `GET <url>` → the user's runs, newest first.
|
|
31
26
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* boundary has none
|
|
35
|
-
*
|
|
36
|
-
* crashed run worth showing, not worth continuing. {@link continuable} filters
|
|
37
|
-
* for exactly that.
|
|
27
|
+
* Only `continuable` rows can be resumed: the server reports whether a run has
|
|
28
|
+
* a saved snapshot to seed from, and a run that never reached a provider-valid
|
|
29
|
+
* boundary has none. Offer the action only for those rows ({@link continuable}
|
|
30
|
+
* filters them) and treat the rest as informational.
|
|
38
31
|
*
|
|
39
|
-
*
|
|
40
|
-
* django-ag-ui mounts
|
|
41
|
-
*
|
|
42
|
-
* all and there is no way to configure a half-working set.
|
|
32
|
+
* Resume and fork are siblings of the index rather than separate configuration
|
|
33
|
+
* — django-ag-ui mounts `runs/`, `resume/<id>/` and `fork/<id>/` under one
|
|
34
|
+
* prefix — so this one URL locates all three.
|
|
43
35
|
*/
|
|
44
36
|
export class RunIndex {
|
|
45
37
|
readonly #url: string;
|
|
@@ -57,10 +49,9 @@ export class RunIndex {
|
|
|
57
49
|
}
|
|
58
50
|
|
|
59
51
|
/**
|
|
60
|
-
* The user's runs, or `[]` when the endpoint is unreachable or
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* the user can do nothing about.
|
|
52
|
+
* The user's runs, or `[]` when the endpoint is unreachable or errors — the
|
|
53
|
+
* caller renders its empty state rather than a transport fault the user can
|
|
54
|
+
* do nothing about.
|
|
64
55
|
*/
|
|
65
56
|
async list(): Promise<readonly RunRow[]> {
|
|
66
57
|
try {
|
|
@@ -102,9 +93,9 @@ export class RunIndex {
|
|
|
102
93
|
/**
|
|
103
94
|
* `<mount>/<verb>/<runId>/`, derived from the index URL's own prefix.
|
|
104
95
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
96
|
+
* String surgery on the trailing `runs/` rather than `new URL`, because the
|
|
97
|
+
* configured value may be root-relative (`/agent/runs/`), the common case in
|
|
98
|
+
* a Django template, and `new URL` needs an absolute base.
|
|
108
99
|
*/
|
|
109
100
|
#sibling(verb: string, runId: string): string {
|
|
110
101
|
const prefix = this.#url.slice(0, -"runs/".length);
|
|
@@ -2,10 +2,10 @@ import { withCredentials } from "./utils.js";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The composer's voice-transcription contract: take a recorded audio `Blob` and
|
|
5
|
-
* resolve to the transcript text. The built-in handler is
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* resolve to the transcript text. The built-in handler is
|
|
6
|
+
* {@link transcribeAudio}; a host swaps in its own (a Web Speech adapter, a
|
|
7
|
+
* direct-to-provider call) via `AgUiChat.transcribeHandler` without touching
|
|
8
|
+
* the mic button.
|
|
9
9
|
*/
|
|
10
10
|
export type TranscribeHandler = (audio: Blob) => Promise<string>;
|
|
11
11
|
|
|
@@ -27,15 +27,14 @@ export interface TranscribeOptions {
|
|
|
27
27
|
/**
|
|
28
28
|
* POST a recorded clip to the transcription endpoint and resolve to its text.
|
|
29
29
|
*
|
|
30
|
-
* The clip
|
|
31
|
-
* `headers`, mirroring {@link uploadAttachment}
|
|
32
|
-
* `{ "text": "<transcript>" }`. A non-2xx response or a network error rejects
|
|
33
|
-
* the mic button can surface the failure.
|
|
30
|
+
* The clip goes as multipart under the `audio` field with the element's
|
|
31
|
+
* `headers`, mirroring {@link uploadAttachment}, and the server replies
|
|
32
|
+
* `{ "text": "<transcript>" }`. A non-2xx response or a network error rejects
|
|
33
|
+
* so the mic button can surface the failure.
|
|
34
34
|
*/
|
|
35
35
|
export async function transcribeAudio(audio: Blob, options: TranscribeOptions): Promise<string> {
|
|
36
36
|
const form = new FormData();
|
|
37
|
-
//
|
|
38
|
-
// reads the blob's content type).
|
|
37
|
+
// The extension is cosmetic: the server reads the blob's content type.
|
|
39
38
|
form.append("audio", audio, "recording.webm");
|
|
40
39
|
|
|
41
40
|
const response = await fetch(
|