@artooi/ag-ui-web-component 0.35.2 → 0.37.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/dist/index.js CHANGED
@@ -41,6 +41,11 @@ var TOOL_CALL_STATUS = {
41
41
  ERROR: "error",
42
42
  DECLINED: "declined"
43
43
  };
44
+ var TOOL_OUTCOME = {
45
+ SUCCESS: "success",
46
+ FAILED: "failed",
47
+ DENIED: "denied"
48
+ };
44
49
  var ATTACHMENT_STATUS = {
45
50
  UPLOADING: "uploading",
46
51
  READY: "ready",
@@ -824,6 +829,8 @@ var DEFAULT_UI_STRINGS = {
824
829
  runInterrupted: "The previous response didn\u2019t finish \u2014 the page changed before it arrived.",
825
830
  pageMoved: "The page changed since you last looked at it. Call read_page to see the current page, then retry.",
826
831
  attachmentsStillUploading: "{n} file still uploading \u2014 it was not sent with this message and is still attached.",
832
+ notConnected: "This chat isn\u2019t connected to an agent, so the message wasn\u2019t sent.",
833
+ continueNeedsTurn: "Type the next turn in the composer first, then pick a run to continue.",
827
834
  skillNeeds: "\u201C{title}\u201D needs {fields} \u2014 fill it in below, then send.",
828
835
  message: "Message",
829
836
  inputPlaceholder: "Ask anything\u2026",
@@ -10779,6 +10786,25 @@ var AgUiClient = class {
10779
10786
  * set would miss entirely.
10780
10787
  */
10781
10788
  #closedMessageIds = /* @__PURE__ */ new Set();
10789
+ /**
10790
+ * How each tool call ended, keyed by call id, for the transcript this client
10791
+ * persists.
10792
+ *
10793
+ * A side table rather than a field written onto the message, because neither
10794
+ * producer of a tool message will carry it. `@ag-ui/client` builds the
10795
+ * server-side one by destructuring five named fields off the event, so an
10796
+ * `outcome` beside them is dropped before the message exists; and writing it
10797
+ * back onto that message afterwards would put it in `agent.messages`, which is
10798
+ * what the *next* request sends to the server. This keeps the annotation on
10799
+ * the copy handed to the store and off the wire.
10800
+ *
10801
+ * Per client, not per run: `saveMessages` rewrites the whole transcript on
10802
+ * every persist, so an outcome recorded in round one has to still be here in
10803
+ * round five or the earlier card silently reverts to a green one. Bounded by
10804
+ * the number of tool calls in the conversation, which the transcript beside it
10805
+ * already is.
10806
+ */
10807
+ #outcomes = /* @__PURE__ */ new Map();
10782
10808
  #connectionLostMessage;
10783
10809
  #maxToolRounds;
10784
10810
  // Set by cancel(); reset at the top of each #run(). Checked by the loop so
@@ -10839,7 +10865,7 @@ var AgUiClient = class {
10839
10865
  message.attachments = attachments;
10840
10866
  }
10841
10867
  this.#agent.addMessage(message);
10842
- this.#onPersist(this.#agent.messages);
10868
+ this.#persist();
10843
10869
  await this.#run();
10844
10870
  }
10845
10871
  /**
@@ -10874,7 +10900,7 @@ var AgUiClient = class {
10874
10900
  }
10875
10901
  const kept = messages.slice(0, lastUser + 1);
10876
10902
  this.#agent.setMessages(kept);
10877
- this.#onPersist(this.#agent.messages);
10903
+ this.#persist();
10878
10904
  return kept;
10879
10905
  }
10880
10906
  /**
@@ -10888,7 +10914,7 @@ var AgUiClient = class {
10888
10914
  /** Append a frontend tool result to history (used by the resume path). */
10889
10915
  addToolResult(toolCallId, content) {
10890
10916
  this.#agent.addMessage({ id: randomUUID2(), role: "tool", content, toolCallId });
10891
- this.#onPersist(this.#agent.messages);
10917
+ this.#persist();
10892
10918
  }
10893
10919
  /**
10894
10920
  * Cancel the in-flight run (AG-UI has no server cancel route — aborting the
@@ -10919,9 +10945,33 @@ var AgUiClient = class {
10919
10945
  }
10920
10946
  }
10921
10947
  #onCancelled() {
10922
- this.#onPersist(this.#agent.messages);
10948
+ this.#persist();
10923
10949
  this.#handlers.onCancelled();
10924
10950
  }
10951
+ /**
10952
+ * Hand the transcript to the host's store, annotated with what {@link #outcomes}
10953
+ * knows about how each tool call ended.
10954
+ *
10955
+ * Every persist in this class goes through here, because the store keeps only
10956
+ * the most recent list: annotating one call site would mean the next
10957
+ * unannotated save quietly threw the annotations away.
10958
+ */
10959
+ #persist() {
10960
+ const messages = this.#agent.messages;
10961
+ if (this.#outcomes.size === 0) {
10962
+ this.#onPersist(messages);
10963
+ return;
10964
+ }
10965
+ this.#onPersist(
10966
+ messages.map((message) => {
10967
+ if (message.role !== "tool") {
10968
+ return message;
10969
+ }
10970
+ const outcome = this.#outcomes.get(message.toolCallId);
10971
+ return outcome === void 0 ? message : { ...message, outcome };
10972
+ })
10973
+ );
10974
+ }
10925
10975
  async #runLoop() {
10926
10976
  let resume;
10927
10977
  for (let round = 0; round < this.#maxToolRounds; round += 1) {
@@ -10939,7 +10989,7 @@ var AgUiClient = class {
10939
10989
  }
10940
10990
  await this.#agent.runAgent(params, this.#buildSubscriber(pending, runState));
10941
10991
  resume = void 0;
10942
- this.#onPersist(this.#agent.messages);
10992
+ this.#persist();
10943
10993
  if (this.#cancelled) {
10944
10994
  return;
10945
10995
  }
@@ -10972,13 +11022,16 @@ var AgUiClient = class {
10972
11022
  if (result.halt === true) {
10973
11023
  return;
10974
11024
  }
11025
+ if (result.outcome !== void 0) {
11026
+ this.#outcomes.set(call.id, result.outcome);
11027
+ }
10975
11028
  this.#agent.addMessage({
10976
11029
  id: randomUUID2(),
10977
11030
  role: "tool",
10978
11031
  content: result.content,
10979
11032
  toolCallId: call.id
10980
11033
  });
10981
- this.#onPersist(this.#agent.messages);
11034
+ this.#persist();
10982
11035
  executed = true;
10983
11036
  }
10984
11037
  if (!executed) {
@@ -10989,6 +11042,7 @@ var AgUiClient = class {
10989
11042
  #buildSubscriber(pending, runState) {
10990
11043
  const h = this.#handlers;
10991
11044
  const closed = this.#closedMessageIds;
11045
+ const outcomes = this.#outcomes;
10992
11046
  const cancelled = () => this.#cancelled;
10993
11047
  const pendingDeltas = /* @__PURE__ */ new Set();
10994
11048
  return {
@@ -11019,7 +11073,11 @@ var AgUiClient = class {
11019
11073
  h.onToolCall(call);
11020
11074
  },
11021
11075
  onToolCallResultEvent({ event }) {
11022
- h.onToolResult(event.toolCallId, event.content);
11076
+ const outcome = event["outcome"];
11077
+ if (typeof outcome === "string") {
11078
+ outcomes.set(event.toolCallId, outcome);
11079
+ }
11080
+ h.onToolResult(event.toolCallId, event.content, outcome);
11023
11081
  },
11024
11082
  onActivitySnapshotEvent({ event, messages }) {
11025
11083
  const known = messages.some(
@@ -11644,6 +11702,17 @@ var RunIndex = class {
11644
11702
  }
11645
11703
  };
11646
11704
 
11705
+ // src/core/tool_outcome.ts
11706
+ function toolStatusFromOutcome(outcome) {
11707
+ if (outcome === TOOL_OUTCOME.FAILED) {
11708
+ return TOOL_CALL_STATUS.ERROR;
11709
+ }
11710
+ if (outcome === TOOL_OUTCOME.DENIED) {
11711
+ return TOOL_CALL_STATUS.DECLINED;
11712
+ }
11713
+ return TOOL_CALL_STATUS.DONE;
11714
+ }
11715
+
11647
11716
  // src/core/transcribe_audio.ts
11648
11717
  async function transcribeAudio(audio, options) {
11649
11718
  const form = new FormData();
@@ -12187,7 +12256,17 @@ var AgUiChat = class extends HTMLElement {
12187
12256
  #checkpoints;
12188
12257
  /** Built lazily from `data-runs-url`; `null` when the host didn't opt in. */
12189
12258
  #runIndex = null;
12190
- #skillHint;
12259
+ /**
12260
+ * The one-line hint above the composer, cleared by the next keystroke.
12261
+ *
12262
+ * Two things write it -- a skill whose template is short of a field, and a
12263
+ * run continuation picked with nothing typed -- and both say the same kind of
12264
+ * thing: what the composer still needs before this can go. Its `part` stays
12265
+ * `skill-hint`, which the skills feature named and the README documents;
12266
+ * renaming a part is breaking, and a second hint element in the same slot
12267
+ * would be worse than one whose name is a release older than its job.
12268
+ */
12269
+ #composerHint;
12191
12270
  /** File-picker button + hidden input + tray slot; the tray mounts on connect. */
12192
12271
  #attachButton;
12193
12272
  #fileInput;
@@ -12377,7 +12456,7 @@ var AgUiChat = class extends HTMLElement {
12377
12456
  this.#input = document.createElement("textarea");
12378
12457
  this.#send = document.createElement("button");
12379
12458
  this.#title = document.createElement("span");
12380
- this.#skillHint = document.createElement("div");
12459
+ this.#composerHint = document.createElement("div");
12381
12460
  this.#attachButton = document.createElement("button");
12382
12461
  this.#fileInput = document.createElement("input");
12383
12462
  this.#attachSlot = document.createElement("div");
@@ -12461,6 +12540,9 @@ var AgUiChat = class extends HTMLElement {
12461
12540
  }
12462
12541
  const content = this.#input.value.trim();
12463
12542
  if (content === "") {
12543
+ this.#composerHint.textContent = this.#strings.continueNeedsTurn;
12544
+ this.#composerHint.hidden = false;
12545
+ this.#input.focus();
12464
12546
  return;
12465
12547
  }
12466
12548
  this.#input.value = "";
@@ -12983,20 +13065,37 @@ var AgUiChat = class extends HTMLElement {
12983
13065
  const value = this.getAttribute(name);
12984
13066
  return value !== null && value !== "false";
12985
13067
  }
12986
- /** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
12987
- #readStringOverrides() {
12988
- const raw = this.getAttribute("data-strings");
13068
+ /**
13069
+ * Parse a JSON-valued attribute, saying so when it will not parse.
13070
+ *
13071
+ * `null` for an absent attribute, and `null` again for one that is not JSON --
13072
+ * but not quietly the second time. Quoting JSON inside an HTML attribute is
13073
+ * fiddly, and the result of getting it wrong is indistinguishable from the
13074
+ * feature being switched off: no chips appear, or the strings stay English,
13075
+ * with nothing anywhere saying why. That is the same failure `data-paste-attach`
13076
+ * already reports for a value it cannot read.
13077
+ *
13078
+ * Console only. A page author's typo is not the reader's business, nothing the
13079
+ * reader did was refused, and the degraded widget is still perfectly usable.
13080
+ */
13081
+ #readJsonAttribute(name) {
13082
+ const raw = this.getAttribute(name);
12989
13083
  if (raw === null) {
12990
- return {};
13084
+ return null;
12991
13085
  }
12992
13086
  try {
12993
- const parsed = JSON.parse(raw);
12994
- if (typeof parsed === "object" && parsed !== null) {
12995
- return parsed;
12996
- }
13087
+ return JSON.parse(raw);
12997
13088
  } catch {
13089
+ console.warn(
13090
+ `<ag-ui-chat>: ${name} is not valid JSON, so it was ignored entirely and the built-in default is being used. Check the quoting -- JSON inside an HTML attribute needs single quotes around the attribute value, or its own double quotes escaped.`
13091
+ );
13092
+ return null;
12998
13093
  }
12999
- return {};
13094
+ }
13095
+ /** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
13096
+ #readStringOverrides() {
13097
+ const parsed = this.#readJsonAttribute("data-strings");
13098
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
13000
13099
  }
13001
13100
  /**
13002
13101
  * Enable the composer's file-upload tray when uploads are possible — either a
@@ -13407,15 +13506,7 @@ ${quoted}`;
13407
13506
  }
13408
13507
  /** Parse the inline `data-skills` JSON catalog (empty when absent/malformed). */
13409
13508
  #readEmbeddedSkills() {
13410
- const raw = this.getAttribute("data-skills");
13411
- if (raw === null) {
13412
- return [];
13413
- }
13414
- try {
13415
- return parseSkills(JSON.parse(raw));
13416
- } catch {
13417
- return [];
13418
- }
13509
+ return parseSkills(this.#readJsonAttribute("data-skills"));
13419
13510
  }
13420
13511
  /** Fetch the backend skills catalog from `data-skills-url`, if set. */
13421
13512
  async #fetchSkills() {
@@ -13454,21 +13545,21 @@ ${quoted}`;
13454
13545
  */
13455
13546
  #applySkill(skill) {
13456
13547
  if (skill.prompt === void 0) {
13457
- this.#skillHint.hidden = true;
13548
+ this.#composerHint.hidden = true;
13458
13549
  void this.sendMessage(`/${skill.name}`);
13459
13550
  return;
13460
13551
  }
13461
13552
  const { text: text4, missing } = fillTemplate(skill.prompt, this.skillContext());
13462
13553
  if (missing.length > 0) {
13463
- this.#skillHint.textContent = this.#strings.skillNeeds.replace("{title}", skill.title).replace("{fields}", missing.join(", "));
13464
- this.#skillHint.hidden = false;
13554
+ this.#composerHint.textContent = this.#strings.skillNeeds.replace("{title}", skill.title).replace("{fields}", missing.join(", "));
13555
+ this.#composerHint.hidden = false;
13465
13556
  this.#input.value = text4;
13466
13557
  this.#autoGrow();
13467
13558
  this.#input.focus();
13468
13559
  this.#selectFirstPlaceholder(text4);
13469
13560
  return;
13470
13561
  }
13471
- this.#skillHint.hidden = true;
13562
+ this.#composerHint.hidden = true;
13472
13563
  this.#input.value = text4;
13473
13564
  this.#autoGrow();
13474
13565
  if (skill.sendImmediately === false) {
@@ -14770,7 +14861,10 @@ ${quoted}`;
14770
14861
  if (message.role === "tool") {
14771
14862
  const card = this.#toolCards.get(message.toolCallId);
14772
14863
  if (card !== void 0) {
14773
- card.settle(TOOL_CALL_STATUS.DONE, message.content);
14864
+ card.settle(
14865
+ toolStatusFromOutcome(message.outcome),
14866
+ message.content
14867
+ );
14774
14868
  }
14775
14869
  }
14776
14870
  }
@@ -14988,9 +15082,9 @@ ${quoted}`;
14988
15082
  }
14989
15083
  void this.#submit();
14990
15084
  });
14991
- this.#skillHint.className = "skill-hint";
14992
- this.#skillHint.setAttribute("part", "skill-hint");
14993
- this.#skillHint.hidden = true;
15085
+ this.#composerHint.className = "skill-hint";
15086
+ this.#composerHint.setAttribute("part", "skill-hint");
15087
+ this.#composerHint.hidden = true;
14994
15088
  this.#attachButton.className = "attach-btn";
14995
15089
  this.#attachButton.type = "button";
14996
15090
  this.#attachButton.setAttribute("part", "attach-button");
@@ -15018,7 +15112,7 @@ ${quoted}`;
15018
15112
  this.#messagesWrap,
15019
15113
  this.#skillsMenu.palette,
15020
15114
  this.#skillsMenu.chips,
15021
- this.#skillHint,
15115
+ this.#composerHint,
15022
15116
  this.#queuedRow,
15023
15117
  this.#attachSlot,
15024
15118
  inputRow,
@@ -15291,10 +15385,16 @@ ${quoted}`;
15291
15385
  #updateEmptyState() {
15292
15386
  this.#emptyWrap.hidden = this.#messages.childElementCount > 1;
15293
15387
  }
15294
- /** Forward input changes to the skills palette and clear any stale hint. */
15388
+ /**
15389
+ * Forward input changes to the skills palette and clear any stale hint.
15390
+ *
15391
+ * Typing is the answer to every hint that surface carries -- a skill short of
15392
+ * a field, a continuation short of its next turn -- so the keystroke that
15393
+ * starts answering it is the right moment to take it down.
15394
+ */
15295
15395
  #onInput() {
15296
15396
  this.#skillsMenu.onInput(this.#input.value);
15297
- this.#skillHint.hidden = true;
15397
+ this.#composerHint.hidden = true;
15298
15398
  this.#autoGrow();
15299
15399
  this.#recallIndex = null;
15300
15400
  }
@@ -15531,8 +15631,36 @@ ${quoted}`;
15531
15631
  })
15532
15632
  );
15533
15633
  }
15634
+ /**
15635
+ * Hand the message to the client, or say why it is going nowhere.
15636
+ *
15637
+ * With no `endpoint` there is nothing to send to, and this used to return
15638
+ * here in silence. That is the worst shape the failure can take, because the
15639
+ * two halves of a send that *did* work have already happened by now: the
15640
+ * user's bubble is on screen and {@link SUBMIT_EVENT} has been dispatched. So
15641
+ * the message looks sent, the composer is empty, the Send button never turns
15642
+ * into Stop, and no request is ever made -- an unanswered question rather
15643
+ * than a broken widget. Nothing reached the console either, so the developer
15644
+ * had no thread to pull and the user had no reason to think anything was
15645
+ * wrong with the page.
15646
+ *
15647
+ * Both audiences are told, because they need different things. The console
15648
+ * carries the developer's version -- the attribute is missing, here is what
15649
+ * to set -- since a missing attribute is a page's mistake and not the
15650
+ * reader's. The transcript carries the reader's, because they are looking at
15651
+ * their own message waiting for a reply that cannot come, and the honest
15652
+ * alternative to one muted line is an indefinite wait.
15653
+ *
15654
+ * Said on every attempt rather than once. Each send is a separate thing the
15655
+ * user asked for and did not get, and a once-only report is silence for every
15656
+ * attempt after the first -- which is the defect again, just later.
15657
+ */
15534
15658
  async #client_send(content, attachments) {
15535
15659
  if (this.endpoint === "") {
15660
+ console.error(
15661
+ '<ag-ui-chat>: no endpoint is set, so this message was not sent and no request was made. Point the element at your AG-UI mount with the endpoint attribute (endpoint="/agent/"), or assign chat.endpoint before sending.'
15662
+ );
15663
+ this.#appendNotice("\u26A0", this.#strings.notConnected, "not-connected");
15536
15664
  return;
15537
15665
  }
15538
15666
  await this.#ensureClient().send(content, attachments);
@@ -15674,7 +15802,7 @@ ${quoted}`;
15674
15802
  const message = this.#strings.pageMoved;
15675
15803
  card.settle(TOOL_CALL_STATUS.ERROR, message);
15676
15804
  this.#showPending();
15677
- return { content: `Error: ${message}`, error: message };
15805
+ return { content: `Error: ${message}`, error: message, outcome: TOOL_OUTCOME.FAILED };
15678
15806
  }
15679
15807
  const rule = await this.#confirmationRule(call, tool);
15680
15808
  if (rule !== null) {
@@ -15699,7 +15827,7 @@ ${quoted}`;
15699
15827
  const message = this.#strings.declinedAction;
15700
15828
  card.settle(TOOL_CALL_STATUS.DECLINED, message);
15701
15829
  this.#showPending();
15702
- return { content: message };
15830
+ return { content: message, outcome: TOOL_OUTCOME.DENIED };
15703
15831
  }
15704
15832
  }
15705
15833
  const navigates = isNavigates(tool.parameters) && this.navigate === null;
@@ -15726,7 +15854,7 @@ ${quoted}`;
15726
15854
  const message = error instanceof Error ? error.message : String(error);
15727
15855
  card.settle(TOOL_CALL_STATUS.ERROR, message);
15728
15856
  this.#showPending();
15729
- return { content: `Error: ${message}`, error: message };
15857
+ return { content: `Error: ${message}`, error: message, outcome: TOOL_OUTCOME.FAILED };
15730
15858
  }
15731
15859
  }
15732
15860
  /**
@@ -15898,12 +16026,12 @@ ${quoted}`;
15898
16026
  onMessagesSnapshot: () => {
15899
16027
  this.#appendNotice("\u{1F504}", this.#strings.historyReplaced, "history-replaced");
15900
16028
  },
15901
- onToolResult: (toolCallId, content) => {
16029
+ onToolResult: (toolCallId, content, outcome) => {
15902
16030
  const card = this.#toolCards.get(toolCallId);
15903
16031
  if (card === void 0) {
15904
16032
  return;
15905
16033
  }
15906
- card.settle(TOOL_CALL_STATUS.DONE, content);
16034
+ card.settle(toolStatusFromOutcome(outcome), content);
15907
16035
  this.#serverSettled.add(toolCallId);
15908
16036
  this.#showPending();
15909
16037
  },
@@ -16539,7 +16667,7 @@ function setControlValue(el2, value) {
16539
16667
  }
16540
16668
 
16541
16669
  // src/version.ts
16542
- var VERSION = "0.35.2";
16670
+ var VERSION = "0.37.0";
16543
16671
  export {
16544
16672
  ATTACHMENT_EVENT,
16545
16673
  AgUiChat,
@@ -16575,6 +16703,7 @@ export {
16575
16703
  TOGGLE_EVENT,
16576
16704
  TOOL_CALL_STATUS,
16577
16705
  TOOL_DISPLAY,
16706
+ TOOL_OUTCOME,
16578
16707
  ToolCallCard,
16579
16708
  UNREAD_EVENT,
16580
16709
  VERSION,
@@ -16628,6 +16757,7 @@ export {
16628
16757
  suggestionPrompts,
16629
16758
  toggleCheckbox,
16630
16759
  toggleControl,
16760
+ toolStatusFromOutcome,
16631
16761
  transcribeAudio,
16632
16762
  typeInto,
16633
16763
  uploadAttachment