@artooi/ag-ui-web-component 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artooi/ag-ui-web-component",
3
- "version": "0.2.0",
3
+ "version": "0.2.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",
@@ -152,6 +152,12 @@ export class AgUiChat extends HTMLElement {
152
152
  readonly #toolRegistry = new ClientToolRegistry();
153
153
  /** Tool-call cards awaiting execution, keyed by call id. */
154
154
  readonly #toolCards = new Map<string, ToolCallCard>();
155
+ /**
156
+ * Call ids whose card was already settled from a streamed server-side result
157
+ * (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
158
+ * the real output with the generic "executed on the server" fallback.
159
+ */
160
+ readonly #serverSettled = new Set<string>();
155
161
  readonly #root: ShadowRoot;
156
162
  readonly #chat: HTMLDivElement;
157
163
  readonly #messages: HTMLDivElement;
@@ -410,6 +416,7 @@ export class AgUiChat extends HTMLElement {
410
416
  this.#streamingBubble = null;
411
417
  this.#hidePending();
412
418
  this.#toolCards.clear();
419
+ this.#serverSettled.clear();
413
420
  this.#initialMessages = [];
414
421
  this.#messages.replaceChildren();
415
422
  this.#threadId = this.conversationStore.threadId();
@@ -435,18 +442,57 @@ export class AgUiChat extends HTMLElement {
435
442
  }
436
443
  }
437
444
 
438
- /** Render a restored message as a chat bubble (text turns only). */
445
+ /**
446
+ * Replay a restored message: text bubbles *and* tool activity. An assistant
447
+ * turn may carry `toolCalls` (rendered as cards) and/or text; a `tool` turn
448
+ * carries a result that settles the matching card. So a refreshed page shows
449
+ * the full transcript — tool calls and their results — not just the prose.
450
+ */
439
451
  #renderHistoricMessage(message: Message): void {
440
- if (typeof message.content !== "string" || message.content === "") {
452
+ const text = typeof message.content === "string" ? message.content : "";
453
+ if (message.role === MESSAGE_ROLE.USER) {
454
+ if (text !== "") {
455
+ this.appendMessage(MESSAGE_ROLE.USER, text);
456
+ }
441
457
  return;
442
458
  }
443
- if (message.role === MESSAGE_ROLE.USER) {
444
- this.appendMessage(MESSAGE_ROLE.USER, message.content);
445
- } else if (message.role === MESSAGE_ROLE.ASSISTANT) {
446
- this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, message.content));
459
+ if (message.role === MESSAGE_ROLE.ASSISTANT) {
460
+ if (text !== "") {
461
+ this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, text));
462
+ }
463
+ const toolCalls = message.toolCalls;
464
+ if (toolCalls !== undefined) {
465
+ for (const call of toolCalls) {
466
+ this.#cardFor({
467
+ id: call.id,
468
+ name: call.function.name,
469
+ args: this.#parseArgs(call.function.arguments),
470
+ });
471
+ }
472
+ }
473
+ return;
474
+ }
475
+ if (message.role === "tool") {
476
+ const card = this.#toolCards.get(message.toolCallId);
477
+ if (card !== undefined) {
478
+ card.settle(TOOL_CALL_STATUS.DONE, message.content);
479
+ }
447
480
  }
448
481
  }
449
482
 
483
+ /** Parse a tool call's JSON `arguments` string from history into an object. */
484
+ #parseArgs(raw: string): Record<string, unknown> {
485
+ try {
486
+ const parsed: unknown = JSON.parse(raw);
487
+ if (typeof parsed === "object" && parsed !== null) {
488
+ return parsed as Record<string, unknown>;
489
+ }
490
+ } catch {
491
+ // Malformed history — fall back to empty args rather than failing replay.
492
+ }
493
+ return {};
494
+ }
495
+
450
496
  /**
451
497
  * Word-by-word reveal for the `word` text-animation mode, applied to a
452
498
  * completed assistant bubble. `fade` is pure CSS (no JS); `none` is a no-op.
@@ -639,8 +685,13 @@ export class AgUiChat extends HTMLElement {
639
685
  const tool = this.#resolveTool(call.name);
640
686
  if (tool === null) {
641
687
  // A server-side tool the server already executed — not ours to re-run.
642
- card.settle(TOOL_CALL_STATUS.DONE, "Executed on the server.");
643
- this.#showPending();
688
+ // Its real output usually arrived via `onToolResult` (TOOL_CALL_RESULT)
689
+ // and already settled the card; only fall back when it didn't. We do NOT
690
+ // show the pending indicator here: a server tool never triggers another
691
+ // client round, so showing it would leave it stuck after the run ended.
692
+ if (!this.#serverSettled.has(call.id)) {
693
+ card.settle(TOOL_CALL_STATUS.DONE, "Executed on the server.");
694
+ }
644
695
  return null;
645
696
  }
646
697
  if (await this.#needsConfirmation(call, tool)) {
@@ -708,6 +759,14 @@ export class AgUiChat extends HTMLElement {
708
759
  this.#hidePending();
709
760
  this.#cardFor(call);
710
761
  },
762
+ onToolResult: (toolCallId, content) => {
763
+ const card = this.#toolCards.get(toolCallId);
764
+ if (card === undefined) {
765
+ return;
766
+ }
767
+ card.settle(TOOL_CALL_STATUS.DONE, content);
768
+ this.#serverSettled.add(toolCallId);
769
+ },
711
770
  onRunEnd: () => {
712
771
  this.#hidePending();
713
772
  this.#send.disabled = false;
@@ -719,6 +778,12 @@ export class AgUiChat extends HTMLElement {
719
778
  this.#send.disabled = false;
720
779
  this.#streamingBubble = null;
721
780
  },
781
+ onSettled: () => {
782
+ // Terminal guarantee: whatever path ended the run, return to rest.
783
+ this.#hidePending();
784
+ this.#send.disabled = false;
785
+ this.#streamingBubble = null;
786
+ },
722
787
  };
723
788
  }
724
789
 
@@ -45,8 +45,21 @@ export interface AgUiClientHandlers {
45
45
  onTextEnd(buffer: string): void;
46
46
  /** Fired when the agent finishes calling a tool (server- or frontend-side). */
47
47
  onToolCall(call: AgUiToolCall): void;
48
+ /**
49
+ * Fired when a server-side tool's result streams back (AG-UI's
50
+ * `TOOL_CALL_RESULT`). Frontend tools don't emit this — the client supplies
51
+ * their result itself — so this is the channel for server-executed output.
52
+ */
53
+ onToolResult(toolCallId: string, content: string): void;
48
54
  onRunEnd(): void;
49
55
  onError(message: string): void;
56
+ /**
57
+ * Fired exactly once when the whole interaction settles — after the run loop
58
+ * ends for any reason (a server-only round, frontend-tool rounds exhausted,
59
+ * or an error). The terminal guarantee that the UI returns to rest (pending
60
+ * indicator cleared, input re-enabled) no matter how the run finished.
61
+ */
62
+ onSettled(): void;
50
63
  }
51
64
 
52
65
  /**
@@ -140,6 +153,8 @@ export class AgUiClient {
140
153
  await this.#runLoop();
141
154
  } catch (error) {
142
155
  this.#handlers.onError(error instanceof Error ? error.message : String(error));
156
+ } finally {
157
+ this.#handlers.onSettled();
143
158
  }
144
159
  }
145
160
 
@@ -201,6 +216,9 @@ export class AgUiClient {
201
216
  pending.push(call);
202
217
  h.onToolCall(call);
203
218
  },
219
+ onToolCallResultEvent({ event }) {
220
+ h.onToolResult(event.toolCallId, event.content);
221
+ },
204
222
  onRunErrorEvent({ event }) {
205
223
  h.onError(event.message);
206
224
  },
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.2.0";
1
+ export const VERSION: string = "0.2.1";