@artooi/ag-ui-web-component 0.38.0 → 0.40.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.
Files changed (142) hide show
  1. package/CHANGELOG.md +515 -1
  2. package/README.md +232 -56
  3. package/dist/ag-ui-web-component.bundle.js +370 -139
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +17 -14
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/activity_registry.d.ts +46 -0
  8. package/dist/core/activity_registry.d.ts.map +1 -0
  9. package/dist/core/ag_ui_chat.d.ts +23 -10
  10. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  11. package/dist/core/agui_client.d.ts +28 -0
  12. package/dist/core/agui_client.d.ts.map +1 -1
  13. package/dist/core/client_seed.d.ts +27 -0
  14. package/dist/core/client_seed.d.ts.map +1 -0
  15. package/dist/core/read_max_tool_rounds.d.ts +9 -0
  16. package/dist/core/read_max_tool_rounds.d.ts.map +1 -0
  17. package/dist/core/run_handlers.d.ts +80 -0
  18. package/dist/core/run_handlers.d.ts.map +1 -0
  19. package/dist/core/storage_scope.d.ts +110 -0
  20. package/dist/core/storage_scope.d.ts.map +1 -0
  21. package/dist/core/tool_outcome.d.ts +2 -3
  22. package/dist/core/tool_outcome.d.ts.map +1 -1
  23. package/dist/core/utils.d.ts +46 -0
  24. package/dist/core/utils.d.ts.map +1 -1
  25. package/dist/index.js +16268 -14747
  26. package/dist/index.js.map +4 -4
  27. package/dist/skills/skill_catalog.d.ts +67 -0
  28. package/dist/skills/skill_catalog.d.ts.map +1 -0
  29. package/dist/skills/skill_name_from.d.ts +12 -0
  30. package/dist/skills/skill_name_from.d.ts.map +1 -0
  31. package/dist/tools/tool_catalog.d.ts +79 -0
  32. package/dist/tools/tool_catalog.d.ts.map +1 -0
  33. package/dist/tools/tool_dispatch.d.ts +104 -0
  34. package/dist/tools/tool_dispatch.d.ts.map +1 -0
  35. package/dist/ui/composer/attachment_tray.d.ts.map +1 -1
  36. package/dist/ui/composer/auto_grow.d.ts +10 -0
  37. package/dist/ui/composer/auto_grow.d.ts.map +1 -0
  38. package/dist/ui/composer/composer_attachments.d.ts +57 -0
  39. package/dist/ui/composer/composer_attachments.d.ts.map +1 -0
  40. package/dist/ui/composer/composer_voice.d.ts +48 -0
  41. package/dist/ui/composer/composer_voice.d.ts.map +1 -0
  42. package/dist/ui/composer/voice_input.d.ts.map +1 -1
  43. package/dist/ui/excerpts/transcript_quote_offer.d.ts +57 -0
  44. package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -0
  45. package/dist/ui/fill_ui_string.d.ts +32 -0
  46. package/dist/ui/fill_ui_string.d.ts.map +1 -0
  47. package/dist/ui/history/conversation_history.d.ts +203 -0
  48. package/dist/ui/history/conversation_history.d.ts.map +1 -0
  49. package/dist/ui/history/relative_time.d.ts.map +1 -1
  50. package/dist/ui/history/thread_drawer.d.ts +0 -14
  51. package/dist/ui/history/thread_drawer.d.ts.map +1 -1
  52. package/dist/ui/interrupts/confirmation_card.d.ts.map +1 -1
  53. package/dist/ui/interrupts/pending_decision.d.ts +23 -0
  54. package/dist/ui/interrupts/pending_decision.d.ts.map +1 -0
  55. package/dist/ui/placement/is_collapsible_placement.d.ts +18 -0
  56. package/dist/ui/placement/is_collapsible_placement.d.ts.map +1 -0
  57. package/dist/ui/placement/is_draggable_placement.d.ts +9 -0
  58. package/dist/ui/placement/is_draggable_placement.d.ts.map +1 -0
  59. package/dist/ui/placement/launcher_drag.d.ts +6 -0
  60. package/dist/ui/placement/launcher_drag.d.ts.map +1 -1
  61. package/dist/ui/placement/panel_placement.d.ts +119 -0
  62. package/dist/ui/placement/panel_placement.d.ts.map +1 -0
  63. package/dist/ui/progress/run_announcer.d.ts +41 -0
  64. package/dist/ui/progress/run_announcer.d.ts.map +1 -0
  65. package/dist/ui/progress/subagent_progress.d.ts +58 -0
  66. package/dist/ui/progress/subagent_progress.d.ts.map +1 -0
  67. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  68. package/dist/ui/shell/adopt_styles.d.ts +23 -0
  69. package/dist/ui/shell/adopt_styles.d.ts.map +1 -0
  70. package/dist/ui/shell/glyph_slot.d.ts +7 -0
  71. package/dist/ui/shell/glyph_slot.d.ts.map +1 -0
  72. package/dist/ui/shell/header_button.d.ts +10 -0
  73. package/dist/ui/shell/header_button.d.ts.map +1 -0
  74. package/dist/ui/shell/icon_element.d.ts +12 -0
  75. package/dist/ui/shell/icon_element.d.ts.map +1 -0
  76. package/dist/ui/shell/is_unread_badge_enabled.d.ts +10 -0
  77. package/dist/ui/shell/is_unread_badge_enabled.d.ts.map +1 -0
  78. package/dist/ui/shell/read_launcher_icon_url.d.ts +8 -0
  79. package/dist/ui/shell/read_launcher_icon_url.d.ts.map +1 -0
  80. package/dist/ui/styles.d.ts +1 -1
  81. package/dist/ui/styles.d.ts.map +1 -1
  82. package/dist/ui/transcript/answer_actions.d.ts +39 -0
  83. package/dist/ui/transcript/answer_actions.d.ts.map +1 -0
  84. package/dist/ui/transcript/answer_stream.d.ts +56 -0
  85. package/dist/ui/transcript/answer_stream.d.ts.map +1 -0
  86. package/dist/ui/transcript/render_or_warn.d.ts +16 -0
  87. package/dist/ui/transcript/render_or_warn.d.ts.map +1 -0
  88. package/dist/ui/transcript/starter_chips.d.ts +21 -0
  89. package/dist/ui/transcript/starter_chips.d.ts.map +1 -0
  90. package/dist/ui/transcript/transcript.d.ts +171 -0
  91. package/dist/ui/transcript/transcript.d.ts.map +1 -0
  92. package/dist/ui/ui_strings.d.ts +29 -1
  93. package/dist/ui/ui_strings.d.ts.map +1 -1
  94. package/package.json +1 -1
  95. package/src/constants.ts +17 -15
  96. package/src/core/activity_registry.ts +133 -0
  97. package/src/core/ag_ui_chat.ts +945 -4419
  98. package/src/core/agui_client.ts +172 -16
  99. package/src/core/client_seed.ts +27 -0
  100. package/src/core/read_max_tool_rounds.ts +13 -0
  101. package/src/core/remote_conversation_store.ts +1 -1
  102. package/src/core/run_handlers.ts +501 -0
  103. package/src/core/storage_scope.ts +279 -0
  104. package/src/core/tool_outcome.ts +5 -3
  105. package/src/core/utils.ts +109 -0
  106. package/src/skills/skill_catalog.ts +166 -0
  107. package/src/skills/skill_name_from.ts +19 -0
  108. package/src/tools/tool_catalog.ts +354 -0
  109. package/src/tools/tool_dispatch.ts +521 -0
  110. package/src/ui/composer/attachment_tray.ts +6 -5
  111. package/src/ui/composer/auto_grow.ts +12 -0
  112. package/src/ui/composer/composer_attachments.ts +324 -0
  113. package/src/ui/composer/composer_voice.ts +93 -0
  114. package/src/ui/composer/voice_input.ts +4 -4
  115. package/src/ui/excerpts/transcript_quote_offer.ts +198 -0
  116. package/src/ui/fill_ui_string.ts +41 -0
  117. package/src/ui/history/conversation_history.ts +699 -0
  118. package/src/ui/history/relative_time.ts +5 -4
  119. package/src/ui/history/thread_drawer.ts +3 -50
  120. package/src/ui/interrupts/confirmation_card.ts +4 -2
  121. package/src/ui/interrupts/pending_decision.ts +34 -0
  122. package/src/ui/placement/is_collapsible_placement.ts +19 -0
  123. package/src/ui/placement/is_draggable_placement.ts +23 -0
  124. package/src/ui/placement/launcher_drag.ts +104 -89
  125. package/src/ui/placement/panel_placement.ts +1130 -0
  126. package/src/ui/progress/run_announcer.ts +67 -0
  127. package/src/ui/progress/subagent_progress.ts +201 -0
  128. package/src/ui/progress/tool_call_card.ts +2 -0
  129. package/src/ui/shell/adopt_styles.ts +28 -0
  130. package/src/ui/shell/glyph_slot.ts +12 -0
  131. package/src/ui/shell/header_button.ts +21 -0
  132. package/src/ui/shell/icon_element.ts +33 -0
  133. package/src/ui/shell/is_unread_badge_enabled.ts +11 -0
  134. package/src/ui/shell/read_launcher_icon_url.ts +9 -0
  135. package/src/ui/styles.ts +301 -70
  136. package/src/ui/transcript/answer_actions.ts +139 -0
  137. package/src/ui/transcript/answer_stream.ts +123 -0
  138. package/src/ui/transcript/render_or_warn.ts +22 -0
  139. package/src/ui/transcript/starter_chips.ts +42 -0
  140. package/src/ui/transcript/transcript.ts +429 -0
  141. package/src/ui/ui_strings.ts +38 -1
  142. package/src/version.ts +1 -1
@@ -6,9 +6,10 @@ import {
6
6
  randomUUID,
7
7
  } from "@ag-ui/client";
8
8
  import type { Context, Interrupt, Message, ResumeEntry, Tool } from "@ag-ui/core";
9
- import { MAX_TOOL_ROUNDS } from "../constants.js";
9
+ import { MAX_TOOL_ROUNDS, TOOL_OUTCOME } from "../constants.js";
10
10
  import type { AttachmentRef } from "./attachment.js";
11
11
  import type { ToolOutcome } from "./tool_outcome.js";
12
+ import { answerUnansweredCalls } from "./utils.js";
12
13
 
13
14
  /** A tool call surfaced to the host by {@link AgUiClient}. */
14
15
  export interface AgUiToolCall {
@@ -231,6 +232,22 @@ export interface AgUiClientConfig extends AgUiRunInputs {
231
232
  * passes its localized string.
232
233
  */
233
234
  connectionLostMessage?: string;
235
+ /**
236
+ * Result content for a tool call the conversation moved past without one: a
237
+ * Stop while the stream was arriving, a round that ended on `RUN_ERROR`, a
238
+ * call no {@link executeTool} answered, or history restored with a call still
239
+ * open. Sent to the agent, so it says only that the call did not finish --
240
+ * nobody refused it. Defaults to an English sentence; the host passes its
241
+ * localized string.
242
+ */
243
+ unfinishedMessage?: string;
244
+ /**
245
+ * Result content for a server-side approval that Stop closed while it was
246
+ * open, which the host's {@link resolveInterrupts} answers as declined.
247
+ * Defaults to `"User declined the action."`; the host passes its localized
248
+ * string.
249
+ */
250
+ declinedMessage?: string;
234
251
  /**
235
252
  * Upper bound on frontend tool-call to re-run rounds within one
236
253
  * {@link AgUiClient.send}. Defaults to {@link MAX_TOOL_ROUNDS}.
@@ -300,9 +317,15 @@ export class AgUiClient {
300
317
  * round five or the earlier card silently reverts to a green one. Bounded by
301
318
  * the number of tool calls in the conversation, which the transcript beside it
302
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.
303
324
  */
304
- readonly #outcomes = new Map<string, string>();
325
+ readonly #outcomes = new Map<string, unknown>();
305
326
  readonly #connectionLostMessage: string;
327
+ readonly #unfinishedMessage: string;
328
+ readonly #declinedMessage: string;
306
329
  readonly #maxToolRounds: number;
307
330
  // Set by cancel(); reset at the top of each #run(). Checked by the loop so
308
331
  // a cancel between frontend-tool rounds doesn't start another round.
@@ -317,12 +340,17 @@ export class AgUiClient {
317
340
  this.#resolveInterrupts = config.resolveInterrupts ?? null;
318
341
  this.#onPersist = config.onPersist ?? (() => {});
319
342
  this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
343
+ this.#unfinishedMessage =
344
+ config.unfinishedMessage ??
345
+ "Not finished: the run ended or moved on before this tool call returned a result.";
346
+ this.#declinedMessage = config.declinedMessage ?? "User declined the action.";
320
347
  // Validated here rather than at each caller, so the element's attribute and
321
348
  // a direct consumer get the same answer. A bound below one -- or a NaN from
322
349
  // an unparseable attribute -- is not a smaller budget but a send that runs
323
350
  // the agent zero times, which would look exactly like a broken endpoint.
324
351
  const rounds = config.maxToolRounds ?? MAX_TOOL_ROUNDS;
325
352
  this.#maxToolRounds = rounds >= 1 ? Math.floor(rounds) : MAX_TOOL_ROUNDS;
353
+ this.#adoptSeededOutcomes();
326
354
  const onStateChanged = config.onStateChanged;
327
355
  if (onStateChanged !== undefined) {
328
356
  // The agent applies STATE_SNAPSHOT / STATE_DELTA itself; subscribing is
@@ -475,6 +503,109 @@ export class AgUiClient {
475
503
  this.#handlers.onCancelled();
476
504
  }
477
505
 
506
+ /**
507
+ * Move the outcome annotations a restored history carries off the agent's
508
+ * messages and into {@link #outcomes}.
509
+ *
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.
516
+ *
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.
521
+ */
522
+ #adoptSeededOutcomes(): void {
523
+ const seeded = this.#agent.messages;
524
+ if (!seeded.some((message) => "outcome" in message)) {
525
+ return;
526
+ }
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
+ }
540
+
541
+ /**
542
+ * Give every tool call in history a result, before a request carries it.
543
+ *
544
+ * Called ahead of every `runAgent`, which makes it the one place the rule is
545
+ * kept for every way a call can be left open -- a live run's Stop or
546
+ * `RUN_ERROR`, a name no tool here owns, a server that sent no result -- and
547
+ * for a client seeded with a restored history just the same. The answer says
548
+ * the call did not finish rather than that anyone declined it: a person
549
+ * declined only where they were asked, and those answers are already here by
550
+ * the time this runs (see {@link #answerStoppedApprovals}).
551
+ *
552
+ * `resuming` names the calls the request is itself answering: a resumed
553
+ * approval runs or denies them server-side, and a result here would answer
554
+ * them twice.
555
+ *
556
+ * The labels go to {@link #outcomes}, never onto the messages, because these
557
+ * messages are the request.
558
+ */
559
+ #answerUnansweredCalls(resuming: ReadonlySet<string>): void {
560
+ const messages = this.#agent.messages;
561
+ const answered = answerUnansweredCalls(
562
+ messages,
563
+ (toolCallId) => {
564
+ this.#outcomes.set(toolCallId, TOOL_OUTCOME.INTERRUPTED);
565
+ return { id: randomUUID(), role: "tool", content: this.#unfinishedMessage, toolCallId };
566
+ },
567
+ resuming,
568
+ );
569
+ if (answered !== messages) {
570
+ this.#agent.setMessages([...answered]);
571
+ this.#persist();
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Answer, as declined, every approval that Stop closed as declined.
577
+ *
578
+ * Stop cancels the resume that would have told the server, so the decline the
579
+ * approval recorded reaches no one unless history carries it. Only calls the
580
+ * host's resolver answered `cancelled` are declined: a person who approved a
581
+ * call and then pressed Stop did not decline it, and
582
+ * {@link #answerUnansweredCalls} answers that one on the next request as not
583
+ * finished.
584
+ *
585
+ * The status check is held by "does not call an approved call declined, since
586
+ * nobody declined it" in `ag_ui_chat_unanswered_tool_calls.test.ts`; the
587
+ * `toolCallId` check is held by the type checker, since a result needs an id
588
+ * to answer.
589
+ */
590
+ #answerStoppedApprovals(
591
+ interrupts: readonly Interrupt[],
592
+ responses: Readonly<Record<string, InterruptResponse>>,
593
+ ): void {
594
+ for (const interrupt of interrupts) {
595
+ const toolCallId = interrupt.toolCallId;
596
+ if (toolCallId !== undefined && responses[interrupt.id]?.status === "cancelled") {
597
+ this.#outcomes.set(toolCallId, TOOL_OUTCOME.DENIED);
598
+ this.#agent.addMessage({
599
+ id: randomUUID(),
600
+ role: "tool",
601
+ content: this.#declinedMessage,
602
+ toolCallId,
603
+ });
604
+ }
605
+ }
606
+ this.#persist();
607
+ }
608
+
478
609
  /**
479
610
  * Hand the transcript to the host's store, annotated with what {@link #outcomes}
480
611
  * knows about how each tool call ended.
@@ -484,27 +615,39 @@ export class AgUiClient {
484
615
  * unannotated save quietly threw the annotations away.
485
616
  */
486
617
  #persist(): void {
618
+ this.#onPersist(this.annotatedMessages);
619
+ }
620
+
621
+ /**
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.
625
+ *
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.
631
+ */
632
+ get annotatedMessages(): readonly Message[] {
487
633
  const messages = this.#agent.messages;
488
634
  if (this.#outcomes.size === 0) {
489
- this.#onPersist(messages);
490
- return;
635
+ return messages;
491
636
  }
492
637
  // A copy, and only of the messages that gain something. `agent.messages` is
493
638
  // the list the next `runAgent` sends back to the server, so writing an extra
494
639
  // field into it would put a client-side annotation on the wire; a store is
495
640
  // allowed to hold more than the protocol does.
496
- this.#onPersist(
497
- messages.map((message) => {
498
- if (message.role !== "tool") {
499
- return message;
500
- }
501
- const outcome = this.#outcomes.get(message.toolCallId);
502
- // Cast at the AG-UI boundary, as the `attachments` augmentation on a
503
- // user message already does: `Message` does not declare the field, and
504
- // the default store round-trips it through `JSON.stringify` verbatim.
505
- return outcome === undefined ? message : ({ ...message, outcome } as Message);
506
- }),
507
- );
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
+ });
508
651
  }
509
652
 
510
653
  async #runLoop(): Promise<void> {
@@ -513,6 +656,7 @@ export class AgUiClient {
513
656
  // continues an unfinished frontend-tool round after a page load; this stays
514
657
  // inside one #run().
515
658
  let resume: ResumeEntry[] | undefined;
659
+ let resuming: ReadonlySet<string> = new Set();
516
660
  for (let round = 0; round < this.#maxToolRounds; round += 1) {
517
661
  // A cancel during the previous round's tool execution lands here: the
518
662
  // running handler completed, but no further round starts.
@@ -528,8 +672,10 @@ export class AgUiClient {
528
672
  if (resume !== undefined) {
529
673
  params.resume = resume;
530
674
  }
675
+ this.#answerUnansweredCalls(resuming);
531
676
  await this.#agent.runAgent(params, this.#buildSubscriber(pending, runState));
532
677
  resume = undefined;
678
+ resuming = new Set();
533
679
  this.#persist();
534
680
  // Cancelled mid-stream: don't execute the tool calls collected before the
535
681
  // abort.
@@ -559,9 +705,11 @@ export class AgUiClient {
559
705
  }
560
706
  const responses = await this.#resolveInterrupts(runState.interrupts);
561
707
  if (this.#cancelled) {
708
+ this.#answerStoppedApprovals(runState.interrupts, responses);
562
709
  return;
563
710
  }
564
711
  resume = buildResumeArray(runState.interrupts, responses);
712
+ resuming = new Set(runState.interrupts.flatMap(({ toolCallId }) => toolCallId ?? []));
565
713
  continue;
566
714
  }
567
715
  if (this.#executeTool === null || pending.length === 0) {
@@ -569,6 +717,14 @@ export class AgUiClient {
569
717
  }
570
718
  let executed = false;
571
719
  for (const call of pending) {
720
+ // A cancel while an earlier call in this round ran lands here. That
721
+ // handler could not be aborted and its result is kept, but the calls
722
+ // after it have not started, and a person who pressed Stop during the
723
+ // first page action did not ask for the second. They are answered as not
724
+ // finished before the next request.
725
+ if (this.#cancelled) {
726
+ return;
727
+ }
572
728
  const result = await this.#executeTool(call);
573
729
  if (result === null) {
574
730
  continue;
@@ -0,0 +1,27 @@
1
+ import type { Message } from "@ag-ui/core";
2
+
3
+ /**
4
+ * What differs between the clients one conversation builds -- its own, and one
5
+ * per checkpoint continuation. Everything else about a client comes from the
6
+ * element's single construction, so an option added for one reaches the other.
7
+ */
8
+ export interface ClientSeed {
9
+ /** Where the client's runs are sent. */
10
+ readonly endpoint: string;
11
+ /** The history the agent starts from, and sends with its first run. */
12
+ readonly initialMessages: readonly Message[];
13
+ /**
14
+ * The conversation this client's own history continues, which every save
15
+ * writes ahead of it.
16
+ *
17
+ * Empty for the conversation's own client, whose history is the whole
18
+ * conversation. A continuation's history is only the turn it adds and its
19
+ * answer -- the endpoint supplies the rest from its snapshot -- and a store
20
+ * keeps one list per thread, so a save of that history alone would replace
21
+ * the conversation with its last exchange, and not saving it lost the
22
+ * exchange on the next reload.
23
+ */
24
+ readonly follows: readonly Message[];
25
+ /** Handed the whole conversation each time the client saves it, as written. */
26
+ readonly onSaved?: (conversation: readonly Message[]) => void;
27
+ }
@@ -0,0 +1,13 @@
1
+ import { MAX_TOOL_ROUNDS } from "../constants.js";
2
+
3
+ /**
4
+ * The tool-round budget from `element`'s `data-max-tool-rounds`, for one send.
5
+ *
6
+ * Anything unparseable becomes `NaN`, which {@link AgUiClient} rejects along
7
+ * with a bound below one -- so the two ways of setting this are validated in
8
+ * one place rather than agreeing by coincidence.
9
+ */
10
+ export function readMaxToolRounds(element: Element): number {
11
+ const attr = element.getAttribute("data-max-tool-rounds");
12
+ return attr === null ? MAX_TOOL_ROUNDS : Number.parseInt(attr, 10);
13
+ }
@@ -165,7 +165,7 @@ export class RemoteConversationStore implements ClientConversationStore {
165
165
  }
166
166
  // A 200 whose body isn't JSON (a proxy's HTML error page, a truncated
167
167
  // stream) must not throw an unhandled rejection that the caller's
168
- // `void #rehydrate()` swallows; fall back to the local cache.
168
+ // `void rehydrate()` swallows; fall back to the local cache.
169
169
  const body = await this.#readJson<{ messages?: readonly Message[] }>(response);
170
170
  if (body === null) {
171
171
  return this.#local.loadMessages(threadId);