@artooi/ag-ui-web-component 0.40.0 → 0.41.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 +54 -1
- package/README.md +46 -24
- package/dist/ag-ui-web-component.bundle.js +118 -87
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +3 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +25 -16
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +11 -4
- package/dist/core/attachment.d.ts.map +1 -1
- package/dist/core/tool_outcome.d.ts +4 -3
- package/dist/core/tool_outcome.d.ts.map +1 -1
- package/dist/core/utils.d.ts +19 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.js +461 -452
- package/dist/index.js.map +4 -4
- package/dist/ui/history/conversation_history.d.ts +1 -1
- package/dist/ui/history/conversation_history.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/constants.ts +3 -2
- package/src/core/agui_client.ts +149 -131
- package/src/core/attachment.ts +13 -5
- package/src/core/tool_outcome.ts +4 -3
- package/src/core/utils.ts +23 -0
- package/src/ui/history/conversation_history.ts +25 -22
- package/src/version.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Message } from "@ag-ui/core";
|
|
2
2
|
import type { ActivityRegistry } from "../../core/activity_registry.js";
|
|
3
3
|
import type { AgUiClient } from "../../core/agui_client.js";
|
|
4
4
|
import type { ClientSeed } from "../../core/client_seed.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation_history.d.ts","sourceRoot":"","sources":["../../../src/ui/history/conversation_history.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"conversation_history.d.ts","sourceRoot":"","sources":["../../../src/ui/history/conversation_history.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artooi/ag-ui-web-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Framework-free <ag-ui-chat> Web Component over the AG-UI protocol. Drop-in chat sidebar with a pluggable client-side tool registry, DOM driver primitives, animations, and destructive-action confirmation modal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"node": ">=22"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@ag-ui/client": ">=0.0
|
|
58
|
-
"@ag-ui/core": ">=0.0
|
|
57
|
+
"@ag-ui/client": ">=1.0.0 <2",
|
|
58
|
+
"@ag-ui/core": ">=1.0.0 <2",
|
|
59
59
|
"dompurify": "^3.4.13",
|
|
60
60
|
"marked": "^18.0.9"
|
|
61
61
|
},
|
package/src/constants.ts
CHANGED
|
@@ -271,7 +271,8 @@ export const TOOL_CALL_STATUS = {
|
|
|
271
271
|
} as const;
|
|
272
272
|
|
|
273
273
|
/**
|
|
274
|
-
* How a tool call ended, as the server states it
|
|
274
|
+
* How a tool call ended, as the server states it under `outcome` in a
|
|
275
|
+
* `TOOL_CALL_RESULT`'s `metadata`.
|
|
275
276
|
*
|
|
276
277
|
* The vocabulary is pydantic-ai's own `ToolReturnPart.outcome`, carried whole
|
|
277
278
|
* rather than re-spelled, so the four repos that pass a refusal along agree on
|
|
@@ -280,7 +281,7 @@ export const TOOL_CALL_STATUS = {
|
|
|
280
281
|
* **Absent means {@link TOOL_OUTCOME.SUCCESS}, and that is load-bearing.** Every
|
|
281
282
|
* server written before the field existed omits it, so a missing field has to
|
|
282
283
|
* render exactly as a plain result did — which is why this is an optional
|
|
283
|
-
*
|
|
284
|
+
* key in the event's metadata and not a required field. `FAILED` is a call that ran
|
|
284
285
|
* and failed; `DENIED` is one a person or a guard refused, so it never ran at
|
|
285
286
|
* all. The two are worth distinguishing on screen because only the second is
|
|
286
287
|
* something the user did. `INTERRUPTED` is a call that produced no result because
|
package/src/core/agui_client.ts
CHANGED
|
@@ -5,7 +5,14 @@ import {
|
|
|
5
5
|
type RunAgentParameters,
|
|
6
6
|
randomUUID,
|
|
7
7
|
} from "@ag-ui/client";
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
type Context,
|
|
10
|
+
contentToText,
|
|
11
|
+
type Interrupt,
|
|
12
|
+
type Message,
|
|
13
|
+
type ResumeEntry,
|
|
14
|
+
type Tool,
|
|
15
|
+
} from "@ag-ui/core";
|
|
9
16
|
import { MAX_TOOL_ROUNDS, TOOL_OUTCOME } from "../constants.js";
|
|
10
17
|
import type { AttachmentRef } from "./attachment.js";
|
|
11
18
|
import type { ToolOutcome } from "./tool_outcome.js";
|
|
@@ -91,11 +98,17 @@ export interface AgUiClientHandlers {
|
|
|
91
98
|
* `TOOL_CALL_RESULT`). Frontend tools don't emit this — the client supplies
|
|
92
99
|
* their result itself — so this is the channel for server-executed output.
|
|
93
100
|
*
|
|
94
|
-
* `outcome` is the event's
|
|
95
|
-
* `unknown` rather than {@link ToolOutcome} because
|
|
96
|
-
*
|
|
97
|
-
* this signature honestly claim to. Read it with
|
|
98
|
-
* which treats `undefined` and anything unrecognised
|
|
101
|
+
* `outcome` is the `outcome` key of the event's `metadata`, forwarded raw.
|
|
102
|
+
* It is `unknown` rather than {@link ToolOutcome} because metadata is open by
|
|
103
|
+
* key: the protocol declares the object and validates nothing inside it, so
|
|
104
|
+
* neither can this signature honestly claim to. Read it with
|
|
105
|
+
* `toolStatusFromOutcome`, which treats `undefined` and anything unrecognised
|
|
106
|
+
* as a success.
|
|
107
|
+
*
|
|
108
|
+
* Only the metadata is read. A server written against `@ag-ui/client` 0.x
|
|
109
|
+
* states the outcome at the top level of the event, and 1.0 strips that key
|
|
110
|
+
* before any subscriber runs, so from such a server this is `undefined` and
|
|
111
|
+
* the card settles as done.
|
|
99
112
|
*
|
|
100
113
|
* Added as a third parameter rather than as a new callback, so an
|
|
101
114
|
* implementation written against the two-parameter form still satisfies this
|
|
@@ -300,29 +313,6 @@ export class AgUiClient {
|
|
|
300
313
|
* set would miss entirely.
|
|
301
314
|
*/
|
|
302
315
|
readonly #closedMessageIds = new Set<string>();
|
|
303
|
-
/**
|
|
304
|
-
* How each tool call ended, keyed by call id, for the transcript this client
|
|
305
|
-
* persists.
|
|
306
|
-
*
|
|
307
|
-
* A side table rather than a field written onto the message, because neither
|
|
308
|
-
* producer of a tool message will carry it. `@ag-ui/client` builds the
|
|
309
|
-
* server-side one by destructuring five named fields off the event, so an
|
|
310
|
-
* `outcome` beside them is dropped before the message exists; and writing it
|
|
311
|
-
* back onto that message afterwards would put it in `agent.messages`, which is
|
|
312
|
-
* what the *next* request sends to the server. This keeps the annotation on
|
|
313
|
-
* the copy handed to the store and off the wire.
|
|
314
|
-
*
|
|
315
|
-
* Per client, not per run: `saveMessages` rewrites the whole transcript on
|
|
316
|
-
* every persist, so an outcome recorded in round one has to still be here in
|
|
317
|
-
* round five or the earlier card silently reverts to a green one. Bounded by
|
|
318
|
-
* the number of tool calls in the conversation, which the transcript beside it
|
|
319
|
-
* already is.
|
|
320
|
-
*
|
|
321
|
-
* Values are `unknown` because a restored seed fills it too (see
|
|
322
|
-
* {@link #adoptSeededOutcomes}), and a store is not trusted to hold only the
|
|
323
|
-
* words this class writes: whatever it held is written back as it was.
|
|
324
|
-
*/
|
|
325
|
-
readonly #outcomes = new Map<string, unknown>();
|
|
326
316
|
readonly #connectionLostMessage: string;
|
|
327
317
|
readonly #unfinishedMessage: string;
|
|
328
318
|
readonly #declinedMessage: string;
|
|
@@ -350,7 +340,7 @@ export class AgUiClient {
|
|
|
350
340
|
// the agent zero times, which would look exactly like a broken endpoint.
|
|
351
341
|
const rounds = config.maxToolRounds ?? MAX_TOOL_ROUNDS;
|
|
352
342
|
this.#maxToolRounds = rounds >= 1 ? Math.floor(rounds) : MAX_TOOL_ROUNDS;
|
|
353
|
-
this.#
|
|
343
|
+
this.#moveSeededKeysIntoMetadata();
|
|
354
344
|
const onStateChanged = config.onStateChanged;
|
|
355
345
|
if (onStateChanged !== undefined) {
|
|
356
346
|
// The agent applies STATE_SNAPSHOT / STATE_DELTA itself; subscribing is
|
|
@@ -392,18 +382,24 @@ export class AgUiClient {
|
|
|
392
382
|
* tools (bounded by {@link AgUiClientConfig.maxToolRounds}, which defaults to
|
|
393
383
|
* {@link MAX_TOOL_ROUNDS}).
|
|
394
384
|
*
|
|
395
|
-
* `attachments` ride
|
|
396
|
-
*
|
|
385
|
+
* `attachments` ride in the user message's `metadata`, under
|
|
386
|
+
* `attachments`, which is how the server learns which files the agent can
|
|
387
|
+
* read and how the default store round-trips them for history replay; see
|
|
397
388
|
* {@link messageAttachments}.
|
|
398
389
|
*/
|
|
399
390
|
async send(content: string, attachments: readonly AttachmentRef[] = []): Promise<void> {
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
391
|
+
// In `metadata` because it is the one field on a message `@ag-ui/client`
|
|
392
|
+
// declares open by key. 1.0 strips every undeclared key from the input it
|
|
393
|
+
// sends, so refs at the top level of the message -- where every release
|
|
394
|
+
// before 1.0 put them -- are removed with a console warning and the server
|
|
395
|
+
// is never told a file was attached. No metadata at all without refs: an
|
|
396
|
+
// empty object would put a field on every plain message that says nothing.
|
|
397
|
+
const message: Message = {
|
|
398
|
+
id: randomUUID(),
|
|
399
|
+
role: "user",
|
|
400
|
+
content,
|
|
401
|
+
...(attachments.length > 0 ? { metadata: { attachments } } : {}),
|
|
402
|
+
};
|
|
407
403
|
this.#agent.addMessage(message);
|
|
408
404
|
this.#persist();
|
|
409
405
|
await this.#run();
|
|
@@ -504,38 +500,31 @@ export class AgUiClient {
|
|
|
504
500
|
}
|
|
505
501
|
|
|
506
502
|
/**
|
|
507
|
-
* Move the
|
|
508
|
-
* messages
|
|
503
|
+
* Move the keys a history stored by an earlier release carries at the top
|
|
504
|
+
* level of its messages into their `metadata`, where this release writes them.
|
|
509
505
|
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
506
|
+
* Earlier releases wrote a tool message's `outcome` and a user message's
|
|
507
|
+
* `attachments` beside the protocol's own fields. `@ag-ui/client` 1.0 strips
|
|
508
|
+
* every undeclared key from the input it sends, so a restored conversation
|
|
509
|
+
* sent as it was stored would lose both on its first request: the server
|
|
510
|
+
* would no longer be told about any file attached before the upgrade, and
|
|
511
|
+
* each strip would print a warning. Moved rather than dropped, so the next
|
|
512
|
+
* save writes the conversation in the shape this release reads first and a
|
|
513
|
+
* reload after it still replays a declined card as declined.
|
|
516
514
|
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
515
|
+
* Merged into the metadata a message already has, with a key already there
|
|
516
|
+
* winning: a message carrying both was last written by the newer writer.
|
|
517
|
+
* `ag_ui_chat_restored_outcome_wire.test.ts` reads the result off the body of
|
|
518
|
+
* a real request. The check before `setMessages` only spares a copy when
|
|
519
|
+
* nothing moves, which is every restore of a store this release wrote;
|
|
520
|
+
* nothing observable depends on it.
|
|
521
521
|
*/
|
|
522
|
-
#
|
|
522
|
+
#moveSeededKeysIntoMetadata(): void {
|
|
523
523
|
const seeded = this.#agent.messages;
|
|
524
|
-
|
|
525
|
-
|
|
524
|
+
const moved = seeded.map(withTopLevelKeysInMetadata);
|
|
525
|
+
if (moved.some((message, index) => message !== seeded[index])) {
|
|
526
|
+
this.#agent.setMessages(moved);
|
|
526
527
|
}
|
|
527
|
-
this.#agent.setMessages(
|
|
528
|
-
seeded.map((message) => {
|
|
529
|
-
if (message.role !== "tool") {
|
|
530
|
-
return message;
|
|
531
|
-
}
|
|
532
|
-
// An unannotated message records `undefined`, which `#persist` already
|
|
533
|
-
// reads as "nothing to write back".
|
|
534
|
-
const { outcome, ...wire } = message as typeof message & { outcome?: unknown };
|
|
535
|
-
this.#outcomes.set(message.toolCallId, outcome);
|
|
536
|
-
return wire;
|
|
537
|
-
}),
|
|
538
|
-
);
|
|
539
528
|
}
|
|
540
529
|
|
|
541
530
|
/**
|
|
@@ -553,17 +542,21 @@ export class AgUiClient {
|
|
|
553
542
|
* approval runs or denies them server-side, and a result here would answer
|
|
554
543
|
* them twice.
|
|
555
544
|
*
|
|
556
|
-
* The
|
|
557
|
-
*
|
|
545
|
+
* The outcome rides in the result's `metadata`, where the client folds a
|
|
546
|
+
* server's own, so the next request carries it and a reload replays it. A
|
|
547
|
+
* server that does not read it ignores it: `metadata` is a declared field.
|
|
558
548
|
*/
|
|
559
549
|
#answerUnansweredCalls(resuming: ReadonlySet<string>): void {
|
|
560
550
|
const messages = this.#agent.messages;
|
|
561
551
|
const answered = answerUnansweredCalls(
|
|
562
552
|
messages,
|
|
563
|
-
(toolCallId) => {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
553
|
+
(toolCallId) => ({
|
|
554
|
+
id: randomUUID(),
|
|
555
|
+
role: "tool",
|
|
556
|
+
content: this.#unfinishedMessage,
|
|
557
|
+
toolCallId,
|
|
558
|
+
metadata: { outcome: TOOL_OUTCOME.INTERRUPTED },
|
|
559
|
+
}),
|
|
567
560
|
resuming,
|
|
568
561
|
);
|
|
569
562
|
if (answered !== messages) {
|
|
@@ -594,12 +587,12 @@ export class AgUiClient {
|
|
|
594
587
|
for (const interrupt of interrupts) {
|
|
595
588
|
const toolCallId = interrupt.toolCallId;
|
|
596
589
|
if (toolCallId !== undefined && responses[interrupt.id]?.status === "cancelled") {
|
|
597
|
-
this.#outcomes.set(toolCallId, TOOL_OUTCOME.DENIED);
|
|
598
590
|
this.#agent.addMessage({
|
|
599
591
|
id: randomUUID(),
|
|
600
592
|
role: "tool",
|
|
601
593
|
content: this.#declinedMessage,
|
|
602
594
|
toolCallId,
|
|
595
|
+
metadata: { outcome: TOOL_OUTCOME.DENIED },
|
|
603
596
|
});
|
|
604
597
|
}
|
|
605
598
|
}
|
|
@@ -607,47 +600,32 @@ export class AgUiClient {
|
|
|
607
600
|
}
|
|
608
601
|
|
|
609
602
|
/**
|
|
610
|
-
* Hand the transcript to the host's store
|
|
611
|
-
* knows about how each tool call ended.
|
|
603
|
+
* Hand the transcript to the host's store.
|
|
612
604
|
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
605
|
+
* The list as the agent holds it, with nothing added: how each tool call
|
|
606
|
+
* ended rides in its result's `metadata`, which the client folds in from a
|
|
607
|
+
* server's `TOOL_CALL_RESULT` and this class writes for the results it makes
|
|
608
|
+
* itself, so the store sees exactly what the next request sends.
|
|
616
609
|
*/
|
|
617
610
|
#persist(): void {
|
|
618
|
-
this.#onPersist(this.
|
|
611
|
+
this.#onPersist(this.#agent.messages);
|
|
619
612
|
}
|
|
620
613
|
|
|
621
614
|
/**
|
|
622
|
-
* The history in the form a save writes it
|
|
623
|
-
*
|
|
624
|
-
* {@link AgUiClientConfig.onPersist} is handed.
|
|
615
|
+
* The history in the form a save writes it, which is now {@link messages}
|
|
616
|
+
* itself.
|
|
625
617
|
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
618
|
+
* It differed while this client kept how each tool call ended in a side table
|
|
619
|
+
* and annotated a copy for the store, because `@ag-ui/client` 0.x kept nothing
|
|
620
|
+
* but five named fields of a result. 1.0 folds a result's `metadata` onto the
|
|
621
|
+
* message it appends, so the outcome is on the message and there is no copy
|
|
622
|
+
* left to make.
|
|
623
|
+
*
|
|
624
|
+
* @deprecated Read {@link messages}. Kept so a caller written against the
|
|
625
|
+
* getter keeps compiling and gets the same list.
|
|
631
626
|
*/
|
|
632
627
|
get annotatedMessages(): readonly Message[] {
|
|
633
|
-
|
|
634
|
-
if (this.#outcomes.size === 0) {
|
|
635
|
-
return messages;
|
|
636
|
-
}
|
|
637
|
-
// A copy, and only of the messages that gain something. `agent.messages` is
|
|
638
|
-
// the list the next `runAgent` sends back to the server, so writing an extra
|
|
639
|
-
// field into it would put a client-side annotation on the wire; a store is
|
|
640
|
-
// allowed to hold more than the protocol does.
|
|
641
|
-
return messages.map((message) => {
|
|
642
|
-
if (message.role !== "tool") {
|
|
643
|
-
return message;
|
|
644
|
-
}
|
|
645
|
-
const outcome = this.#outcomes.get(message.toolCallId);
|
|
646
|
-
// Cast at the AG-UI boundary, as the `attachments` augmentation on a
|
|
647
|
-
// user message already does: `Message` does not declare the field, and
|
|
648
|
-
// the default store round-trips it through `JSON.stringify` verbatim.
|
|
649
|
-
return outcome === undefined ? message : ({ ...message, outcome } as Message);
|
|
650
|
-
});
|
|
628
|
+
return this.messages;
|
|
651
629
|
}
|
|
652
630
|
|
|
653
631
|
async #runLoop(): Promise<void> {
|
|
@@ -734,19 +712,18 @@ export class AgUiClient {
|
|
|
734
712
|
// next mount. Stop here rather than re-running into a dead context.
|
|
735
713
|
return;
|
|
736
714
|
}
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
this.#outcomes.set(call.id, result.outcome);
|
|
744
|
-
}
|
|
715
|
+
// A frontend tool's refusal or failure is stated by no server, because
|
|
716
|
+
// no server ran the call, so the client writes it where the client
|
|
717
|
+
// folds a server's: the result's `metadata`. Written with the message,
|
|
718
|
+
// so the very first save of it already carries how it ended, and a
|
|
719
|
+
// reload reads the card off it. A success writes no metadata at all,
|
|
720
|
+
// as a server's success states none.
|
|
745
721
|
this.#agent.addMessage({
|
|
746
722
|
id: randomUUID(),
|
|
747
723
|
role: "tool",
|
|
748
724
|
content: result.content,
|
|
749
725
|
toolCallId: call.id,
|
|
726
|
+
...(result.outcome === undefined ? {} : { metadata: { outcome: result.outcome } }),
|
|
750
727
|
});
|
|
751
728
|
this.#persist();
|
|
752
729
|
executed = true;
|
|
@@ -760,7 +737,6 @@ export class AgUiClient {
|
|
|
760
737
|
#buildSubscriber(pending: AgUiToolCall[], runState: RunState): AgentSubscriber {
|
|
761
738
|
const h = this.#handlers;
|
|
762
739
|
const closed = this.#closedMessageIds;
|
|
763
|
-
const outcomes = this.#outcomes;
|
|
764
740
|
// Read at event time, not captured now: the flag flips mid-run, and the
|
|
765
741
|
// subscriber is built before the run that a later `cancel()` stops.
|
|
766
742
|
const cancelled = (): boolean => this.#cancelled;
|
|
@@ -802,20 +778,22 @@ export class AgUiClient {
|
|
|
802
778
|
h.onToolCall(call);
|
|
803
779
|
},
|
|
804
780
|
onToolCallResultEvent({ event }) {
|
|
805
|
-
//
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
781
|
+
// In the event's `metadata`, the one place on it `@ag-ui/client` 1.0
|
|
782
|
+
// declares open by key. Its enforcement stage strips every undeclared
|
|
783
|
+
// key before a subscriber runs, so an `outcome` stated at the top level
|
|
784
|
+
// of the event -- where servers wrote it for 0.x -- never gets here.
|
|
785
|
+
//
|
|
786
|
+
// Nothing is recorded for the transcript: after this returns, the
|
|
787
|
+
// client builds the tool message and folds the event's metadata onto
|
|
788
|
+
// it, so the outcome is already on the message a save writes, word for
|
|
789
|
+
// word as the server said it. A reload reads it back through the same
|
|
790
|
+
// mapping as the live path, which is what keeps the two agreeing.
|
|
791
|
+
const outcome: unknown = event.metadata?.["outcome"];
|
|
792
|
+
// Content may arrive as parts since the protocol's 1.0, so a tool can
|
|
793
|
+
// return an image beside its text. A card shows text, so the text parts
|
|
794
|
+
// are joined and the rest left to the host store, which keeps the
|
|
795
|
+
// message whole; a plain string passes through unchanged.
|
|
796
|
+
h.onToolResult(event.toolCallId, contentToText(event.content), outcome);
|
|
819
797
|
},
|
|
820
798
|
onActivitySnapshotEvent({ event, messages }) {
|
|
821
799
|
// A snapshot for an id already in the list is a replacement, not a new
|
|
@@ -876,9 +854,11 @@ export class AgUiClient {
|
|
|
876
854
|
}
|
|
877
855
|
pendingDeltas.clear();
|
|
878
856
|
},
|
|
879
|
-
// `@ag-ui/client`
|
|
880
|
-
//
|
|
881
|
-
//
|
|
857
|
+
// THINKING_* left the protocol in its 1.0, but `@ag-ui/client` still
|
|
858
|
+
// converts a server's THINKING_* events onto these REASONING_* callbacks,
|
|
859
|
+
// with a console warning, for a deprecation window it sets. Nothing in the
|
|
860
|
+
// family emits THINKING_*: pydantic-ai has sent REASONING_* at every
|
|
861
|
+
// protocol version django-ag-ui admits.
|
|
882
862
|
onReasoningStartEvent() {
|
|
883
863
|
h.onReasoningStart();
|
|
884
864
|
},
|
|
@@ -958,3 +938,41 @@ function isAbortError(error: unknown): boolean {
|
|
|
958
938
|
error.name === "AbortError" || (error instanceof TypeError && /abort/i.test(error.message))
|
|
959
939
|
);
|
|
960
940
|
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* `message` with the keys an earlier release wrote at its top level -- a tool
|
|
944
|
+
* message's `outcome`, a user message's `attachments` -- moved into its
|
|
945
|
+
* `metadata`, or `message` itself when it carries neither.
|
|
946
|
+
*
|
|
947
|
+
* Both keys move whatever the role: no message the protocol defines declares
|
|
948
|
+
* either, so a top-level one can only be this component's own. A key already in
|
|
949
|
+
* the metadata wins over the one being moved, and a `metadata` that is not an
|
|
950
|
+
* object is replaced rather than spread -- a store is not trusted to hold the
|
|
951
|
+
* shape anything here wrote, and a spread string would scatter into numbered
|
|
952
|
+
* keys.
|
|
953
|
+
*
|
|
954
|
+
* Held by "carries history stored by an earlier release into metadata" in
|
|
955
|
+
* `agui_client.test.ts`, which seeds a message with only `attachments`, one
|
|
956
|
+
* with only `outcome`, one whose metadata already has the key, and one whose
|
|
957
|
+
* metadata is a string: deleting either half of the early return, or the
|
|
958
|
+
* object check, fails it.
|
|
959
|
+
*/
|
|
960
|
+
function withTopLevelKeysInMetadata(message: Message): Message {
|
|
961
|
+
const { outcome, attachments, ...wire } = message as Message & {
|
|
962
|
+
outcome?: unknown;
|
|
963
|
+
attachments?: unknown;
|
|
964
|
+
};
|
|
965
|
+
if (outcome === undefined && attachments === undefined) {
|
|
966
|
+
return message;
|
|
967
|
+
}
|
|
968
|
+
const existing = (wire as { metadata?: unknown }).metadata;
|
|
969
|
+
return {
|
|
970
|
+
...wire,
|
|
971
|
+
metadata: {
|
|
972
|
+
...(outcome === undefined ? {} : { outcome }),
|
|
973
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
974
|
+
// `null` passes the check and spreads as nothing, which is what it means.
|
|
975
|
+
...(typeof existing === "object" ? existing : {}),
|
|
976
|
+
},
|
|
977
|
+
} as Message;
|
|
978
|
+
}
|
package/src/core/attachment.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message } from "@ag-ui/core";
|
|
2
|
+
import { metadataOrTopLevel } from "./utils.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A durable, lightweight reference to one uploaded file — what an upload
|
|
@@ -24,17 +25,24 @@ export interface AttachmentRef {
|
|
|
24
25
|
/**
|
|
25
26
|
* The attachment refs a user message carries.
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* `AgUiClient.send` writes them into the message's `metadata`, under
|
|
29
|
+
* `attachments`. That is the one place they survive the trip: `@ag-ui/client`
|
|
30
|
+
* 1.0 strips every key its schemas do not declare from the outgoing input, so
|
|
31
|
+
* refs written at the top level of the message never reach the server, while
|
|
32
|
+
* `metadata` is declared open by key and sent as it is. The server reads them
|
|
33
|
+
* from there to tell the agent which files it can read, and the default store
|
|
34
|
+
* round-trips them, so a restored conversation re-renders its chips.
|
|
35
|
+
*
|
|
36
|
+
* A conversation stored by an earlier release has them at the top level, so
|
|
37
|
+
* that is read when the metadata has none. An older server reads only the top
|
|
38
|
+
* level, so against one the agent is never told about a file at all.
|
|
31
39
|
*
|
|
32
40
|
* Storage is untrusted — hand-edited, truncated, or corrupted — so malformed
|
|
33
41
|
* entries are dropped here; a shapeless one would throw in `iconFor` and abort
|
|
34
42
|
* the whole history replay.
|
|
35
43
|
*/
|
|
36
44
|
export function messageAttachments(message: Message): readonly AttachmentRef[] {
|
|
37
|
-
const refs = (message
|
|
45
|
+
const refs = metadataOrTopLevel(message, "attachments");
|
|
38
46
|
return Array.isArray(refs) ? refs.filter(isAttachmentRef) : [];
|
|
39
47
|
}
|
|
40
48
|
|
package/src/core/tool_outcome.ts
CHANGED
|
@@ -13,9 +13,10 @@ export type ToolOutcome = (typeof TOOL_OUTCOME)[keyof typeof TOOL_OUTCOME];
|
|
|
13
13
|
* The card status a wire outcome settles into.
|
|
14
14
|
*
|
|
15
15
|
* Takes `unknown` rather than {@link ToolOutcome} on purpose: both callers read
|
|
16
|
-
* this off a boundary the type system does not police -- a
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* this off a boundary the type system does not police -- a key in an AG-UI
|
|
17
|
+
* event's `metadata`, which the protocol declares open and does not validate,
|
|
18
|
+
* and a JSON blob out of the conversation store -- so the narrowing belongs
|
|
19
|
+
* here, once, instead of at each of them.
|
|
19
20
|
*
|
|
20
21
|
* **Everything unrecognised maps to `DONE`.** Not because unknown values are
|
|
21
22
|
* expected to be successes, but because the alternative is worse in the
|
package/src/core/utils.ts
CHANGED
|
@@ -205,3 +205,26 @@ function toolCallIds(value: unknown): string[] {
|
|
|
205
205
|
.filter((id): id is string => typeof id === "string")
|
|
206
206
|
: [];
|
|
207
207
|
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A key a message carries in its `metadata`, or the same key at the message's
|
|
211
|
+
* top level when its metadata has none.
|
|
212
|
+
*
|
|
213
|
+
* Two keys are read this way, a tool message's `outcome` and a user message's
|
|
214
|
+
* `attachments`, and both for the same reason. `@ag-ui/client` 1.0 strips every
|
|
215
|
+
* key its schemas do not declare, off an inbound event and off the outgoing
|
|
216
|
+
* input, and `metadata` is the one place it declares open by key, so that is
|
|
217
|
+
* where this component writes them. A conversation stored by an earlier release
|
|
218
|
+
* has them at the top level instead, and a reload has to replay it as it
|
|
219
|
+
* settled.
|
|
220
|
+
*
|
|
221
|
+
* Metadata first, so a message carrying both says what the newer writer said.
|
|
222
|
+
* The result is `unknown` because it comes out of a store, which is not trusted
|
|
223
|
+
* to hold the shape anything here wrote; the caller narrows it. `Object()` boxes
|
|
224
|
+
* an absent, `null` or primitive `metadata` into an object with neither key,
|
|
225
|
+
* which reads as no metadata without a branch per shape.
|
|
226
|
+
*/
|
|
227
|
+
export function metadataOrTopLevel(message: Message, key: "outcome" | "attachments"): unknown {
|
|
228
|
+
const metadata = Object((message as { metadata?: unknown }).metadata) as Record<string, unknown>;
|
|
229
|
+
return metadata[key] ?? (message as unknown as Record<string, unknown>)[key];
|
|
230
|
+
}
|