@artooi/ag-ui-web-component 0.40.0 → 0.41.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.
@@ -1,4 +1,4 @@
1
- import type { Message } from "@ag-ui/core";
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":"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"}
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.40.0",
3
+ "version": "0.41.1",
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.59 <0.1",
58
- "@ag-ui/core": ">=0.0.59 <0.1",
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 on `TOOL_CALL_RESULT`.
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
- * annotation on the event and not a required one. `FAILED` is a call that ran
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
@@ -5,7 +5,14 @@ import {
5
5
  type RunAgentParameters,
6
6
  randomUUID,
7
7
  } from "@ag-ui/client";
8
- import type { Context, Interrupt, Message, ResumeEntry, Tool } from "@ag-ui/core";
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 optional `outcome` field, forwarded raw. It is
95
- * `unknown` rather than {@link ToolOutcome} because it comes off a
96
- * `passthrough` zod schema: the protocol does not validate it, so neither can
97
- * this signature honestly claim to. Read it with `toolStatusFromOutcome`,
98
- * which treats `undefined` and anything unrecognised as a success.
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.#adoptSeededOutcomes();
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 on the user message as a non-standard field so the
396
- * default store round-trips them for history replay; see
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
- // Cast at the AG-UI boundary: `attachments` is a web-component augmentation
401
- // the strict `Message` union doesn't declare, but `addMessage` /
402
- // `structuredClone` preserve it verbatim.
403
- const message = { id: randomUUID(), role: "user", content } as Message;
404
- if (attachments.length > 0) {
405
- (message as { attachments?: readonly AttachmentRef[] }).attachments = attachments;
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 outcome annotations a restored history carries off the agent's
508
- * messages and into {@link #outcomes}.
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
- * A restore seeds the agent from the stored copy, and the stored copy is the
511
- * annotated one {@link #persist} wrote. Left on `agent.messages`, the
512
- * annotation went out on the very next request -- the one place it was
513
- * promised never to go. Moved rather than dropped: the store keeps the whole
514
- * transcript as one list, so the next save has to write each annotation back
515
- * or a declined card turns green on the reload after.
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
- * `ag_ui_chat_restored_outcome_wire.test.ts` holds the move, off the body of a
518
- * real request. The early return only spares the copy for a seed with nothing
519
- * to move, and the role check is held by the type checker: only a tool message
520
- * has the `toolCallId` the annotation is keyed by.
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
- #adoptSeededOutcomes(): void {
522
+ #moveSeededKeysIntoMetadata(): void {
523
523
  const seeded = this.#agent.messages;
524
- if (!seeded.some((message) => "outcome" in message)) {
525
- return;
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 labels go to {@link #outcomes}, never onto the messages, because these
557
- * messages are the request.
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
- this.#outcomes.set(toolCallId, TOOL_OUTCOME.INTERRUPTED);
565
- return { id: randomUUID(), role: "tool", content: this.#unfinishedMessage, toolCallId };
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, annotated with what {@link #outcomes}
611
- * knows about how each tool call ended.
603
+ * Hand the transcript to the host's store.
612
604
  *
613
- * Every persist in this class goes through here, because the store keeps only
614
- * the most recent list: annotating one call site would mean the next
615
- * unannotated save quietly threw the annotations away.
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.annotatedMessages);
611
+ this.#onPersist(this.#agent.messages);
619
612
  }
620
613
 
621
614
  /**
622
- * The history in the form a save writes it: {@link messages}, with how each
623
- * tool call ended annotated onto its result -- exactly what
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
- * For writing the conversation somewhere other than a save. The element needs
627
- * it when a checkpoint continuation adds to a conversation this client holds:
628
- * the continuation's saves write the conversation ahead of the exchange, and
629
- * the bare {@link messages} would drop the annotations, so a declined card
630
- * turned green on the next reload.
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
- const messages = this.#agent.messages;
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
- // Recorded before the persist below, so the very first save of this
738
- // message already carries how it ended. A frontend tool's refusal or
739
- // failure never touches the wire's `outcome` field -- no server states
740
- // it, because no server ran the call -- so this side table is the only
741
- // record there is, and a reload reads a card off it.
742
- if (result.outcome !== undefined) {
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
- // Bracket access because the field is not declared: `TOOL_CALL_RESULT`
806
- // extends a `passthrough` schema, so an unknown key survives parsing and
807
- // arrives here typed only by the catch-all index signature. That is the
808
- // whole mechanism the outcome rides -- no schema change in `@ag-ui/core`
809
- // is needed for a server to state one.
810
- const outcome = event["outcome"];
811
- // Recorded even when it is a word this client does not recognise, and
812
- // even when it says "success": the store is a record of what the server
813
- // said, and re-reading it through the same mapping as the live path is
814
- // what keeps a reload agreeing with what the user watched happen.
815
- if (typeof outcome === "string") {
816
- outcomes.set(event.toolCallId, outcome);
817
- }
818
- h.onToolResult(event.toolCallId, event.content, outcome);
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` maps the deprecated THINKING_* events onto these
880
- // REASONING_* callbacks, so the reasoning family alone covers both
881
- // protocol versions.
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
+ }
@@ -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
- * 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.
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 as { attachments?: unknown }).attachments;
45
+ const refs = metadataOrTopLevel(message, "attachments");
38
46
  return Array.isArray(refs) ? refs.filter(isAttachmentRef) : [];
39
47
  }
40
48
 
@@ -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 `passthrough` field
17
- * on an AG-UI event, and a JSON blob out of the conversation store -- so the
18
- * narrowing belongs here, once, instead of at each of them.
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
+ }