@artooi/ag-ui-web-component 0.31.0 → 0.32.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.
@@ -22,6 +22,7 @@ import {
22
22
  RUN_FINISHED_EVENT,
23
23
  STATE_EVENT,
24
24
  SUBAGENT_CUSTOM_NAME,
25
+ SUBAGENT_PHASE,
25
26
  SUBMIT_EVENT,
26
27
  SUGGESTIONS_ACTIVITY_TYPE,
27
28
  TOGGLE_EVENT,
@@ -82,7 +83,7 @@ import { renderRunNotice } from "../ui/run_notice.js";
82
83
  import { SkillsMenu } from "../ui/skills_menu.js";
83
84
  import { createStickToBottom, type StickToBottom } from "../ui/stick_to_bottom.js";
84
85
  import { STYLES } from "../ui/styles.js";
85
- import { SubAgentPanel } from "../ui/subagent_panel.js";
86
+ import { SubAgentPanel, type SubAgentPhase, type SubAgentUpdate } from "../ui/subagent_panel.js";
86
87
  import { subAgentUpdate } from "../ui/subagent_update.js";
87
88
  import { renderSuggestionChips } from "../ui/suggestion_chips.js";
88
89
  import { ThoughtsBlock } from "../ui/thoughts_block.js";
@@ -630,6 +631,17 @@ export class AgUiChat extends HTMLElement {
630
631
  * approval prompt already uses.
631
632
  */
632
633
  readonly #subagentPanels = new Map<string, SubAgentPanel>();
634
+ /**
635
+ * Which delegation each live `subagentRunId` belongs to.
636
+ *
637
+ * The protocol's closing events -- `SUBAGENT_FINISHED` and `SUBAGENT_ERROR`
638
+ * -- carry the child's run id and nothing else, while everything drawn here
639
+ * is keyed on the parent's `delegate_task` call id. `SUBAGENT_STARTED` is the
640
+ * one event carrying both, so the pairing is recorded there and read back on
641
+ * the close. A close naming a run this never saw open is dropped, which is
642
+ * the same refusal a step for an undrawn card gets.
643
+ */
644
+ readonly #subagentRunDelegations = new Map<string, string>();
633
645
  /**
634
646
  * Call ids whose card was already settled from a streamed server-side result
635
647
  * (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
@@ -2473,6 +2485,7 @@ export class AgUiChat extends HTMLElement {
2473
2485
  // the correct half of that split -- a delegation that was live before this
2474
2486
  // transcript was wiped is not live now.
2475
2487
  this.#subagentPanels.clear();
2488
+ this.#subagentRunDelegations.clear();
2476
2489
  this.#serverSettled.clear();
2477
2490
  this.#cardElements.clear();
2478
2491
  this.#activityBlocks.clear();
@@ -3945,6 +3958,32 @@ export class AgUiChat extends HTMLElement {
3945
3958
  }),
3946
3959
  );
3947
3960
  },
3961
+ // The delegation's own lifetime, on the protocol's events rather than the
3962
+ // CUSTOM channel its steps ride. Both end at the same panel.
3963
+ onSubAgentStarted: (subagentRunId, agent, parentToolCallId) => {
3964
+ // A delegation naming no parent call names no card, and a floating
3965
+ // panel is exactly what attaching to the card was chosen over.
3966
+ if (parentToolCallId === null) {
3967
+ return;
3968
+ }
3969
+ this.#subagentRunDelegations.set(subagentRunId, parentToolCallId);
3970
+ this.#applySubAgent({
3971
+ delegationId: parentToolCallId,
3972
+ agent: agent === "" ? null : agent,
3973
+ phase: SUBAGENT_PHASE.STARTED,
3974
+ status: this.#strings.subAgentDelegatedTo.replace("{agent}", agent),
3975
+ tool: null,
3976
+ });
3977
+ },
3978
+ onSubAgentFinished: (subagentRunId) => {
3979
+ this.#closeSubAgent(subagentRunId, SUBAGENT_PHASE.FINISHED, null);
3980
+ },
3981
+ onSubAgentError: (subagentRunId, message) => {
3982
+ // The server's own words, which the contract keeps to the sub-agent's
3983
+ // name. Passed through as the status line and set with textContent
3984
+ // downstream, never parsed as markup.
3985
+ this.#closeSubAgent(subagentRunId, SUBAGENT_PHASE.FAILED, message);
3986
+ },
3948
3987
  onMessagesSnapshot: () => {
3949
3988
  // Honoured for persistence and announced, not re-rendered.
3950
3989
  //
@@ -4144,6 +4183,57 @@ export class AgUiChat extends HTMLElement {
4144
4183
  if (update === null) {
4145
4184
  return;
4146
4185
  }
4186
+ this.#applySubAgent(update);
4187
+ }
4188
+
4189
+ /**
4190
+ * Fold one already-narrowed update into the delegation's panel.
4191
+ *
4192
+ * The join point of the two carriers, and the reason it is separate from
4193
+ * {@link #reportSubAgent}: a `CUSTOM` step arrives as `unknown` and has to be
4194
+ * vouched for, while a lifecycle event arrives typed off the protocol and has
4195
+ * nothing left to check. Both end up here, so the panel has one way in and
4196
+ * the phases stay a single state machine regardless of which wire they came
4197
+ * from.
4198
+ */
4199
+ /**
4200
+ * Settle the delegation a closing lifecycle event names.
4201
+ *
4202
+ * `status` is the server's text on a failure and `null` on a success, where
4203
+ * the wording is this element's own -- the protocol's finish event carries no
4204
+ * message, which is the better shape for a localised UI and the reason
4205
+ * {@link UiStrings.subAgentFinished} exists.
4206
+ *
4207
+ * The pairing is deliberately not deleted on close. A panel outlives the
4208
+ * delegation it drew, the map is cleared with the transcript alongside the
4209
+ * panels, and forgetting the id here would only make a duplicate close draw
4210
+ * nothing instead of drawing the same settled row again.
4211
+ */
4212
+ #closeSubAgent(subagentRunId: string, phase: SubAgentPhase, status: string | null): void {
4213
+ const delegationId = this.#subagentRunDelegations.get(subagentRunId);
4214
+ if (delegationId === undefined) {
4215
+ // A close naming a delegation this never saw open -- the same refusal a
4216
+ // step for an undrawn card gets, and the same reason.
4217
+ return;
4218
+ }
4219
+ const agent = this.#subagentPanels.get(delegationId)?.agent ?? null;
4220
+ this.#applySubAgent({
4221
+ delegationId,
4222
+ agent,
4223
+ phase,
4224
+ status: status === null ? this.#finishedLine(agent) : status,
4225
+ tool: null,
4226
+ });
4227
+ }
4228
+
4229
+ /** The row's line for a delegation that completed, named if its name is known. */
4230
+ #finishedLine(agent: string | null): string {
4231
+ return agent === null
4232
+ ? this.#strings.subAgentWorking
4233
+ : this.#strings.subAgentFinished.replace("{agent}", agent);
4234
+ }
4235
+
4236
+ #applySubAgent(update: SubAgentUpdate): void {
4147
4237
  const card = this.#toolCards.get(update.delegationId);
4148
4238
  if (card === undefined) {
4149
4239
  return;
@@ -127,6 +127,31 @@ export interface AgUiClientHandlers {
127
127
  * be the thing the open field exists to avoid.
128
128
  */
129
129
  onCustomEvent(name: string, value: unknown): void;
130
+ /**
131
+ * A delegated sub-agent started, inside the run.
132
+ *
133
+ * The protocol's own event, unlike the steps that follow it on
134
+ * {@link SUBAGENT_CUSTOM_NAME}. `parentToolCallId` links the delegation to the
135
+ * `delegate_task` call that spawned it -- the field AG-UI provides for the
136
+ * "agents as tools" shape -- and is `null` when the server did not send one,
137
+ * which a host reads as a delegation it has nothing to attach to.
138
+ *
139
+ * `subagentRunId` names the child run and is the only id the two closing
140
+ * events carry, so a host that wants to close what it opened has to remember
141
+ * the pairing.
142
+ */
143
+ onSubAgentStarted(subagentRunId: string, name: string, parentToolCallId: string | null): void;
144
+ /** The delegation named by `subagentRunId` completed. */
145
+ onSubAgentFinished(subagentRunId: string): void;
146
+ /**
147
+ * The delegation named by `subagentRunId` failed.
148
+ *
149
+ * `message` is server text and required by the protocol, but it is not an
150
+ * exception's words: `django-ag-ui` sends only which sub-agent failed, on the
151
+ * same reasoning that redacts a `RUN_ERROR`. Render it as text, never as
152
+ * markup.
153
+ */
154
+ onSubAgentError(subagentRunId: string, message: string): void;
130
155
  onError(message: string): void;
131
156
  /**
132
157
  * Fired when the user cancelled the run ({@link AgUiClient.cancel}) — the
@@ -567,6 +592,22 @@ export class AgUiClient {
567
592
  onCustomEvent({ event }) {
568
593
  h.onCustomEvent(event.name, event.value);
569
594
  },
595
+ // The delegation lifecycle. Forwarded rather than interpreted here, for
596
+ // the reason every other subscriber in this block is thin: this file
597
+ // adapts the protocol to the handler surface, and what a delegation looks
598
+ // like on screen is the element's business.
599
+ //
600
+ // `parentToolCallId` is optional on the wire and normalised to `null` so
601
+ // the handler has one absent-value to check rather than two.
602
+ onSubagentStartedEvent({ event }) {
603
+ h.onSubAgentStarted(event.subagentRunId, event.name, event.parentToolCallId ?? null);
604
+ },
605
+ onSubagentFinishedEvent({ event }) {
606
+ h.onSubAgentFinished(event.subagentRunId);
607
+ },
608
+ onSubagentErrorEvent({ event }) {
609
+ h.onSubAgentError(event.subagentRunId, event.message);
610
+ },
570
611
  onMessagesSnapshotEvent({ event }) {
571
612
  h.onMessagesSnapshot(event.messages as readonly Message[]);
572
613
  },
package/src/ui/styles.ts CHANGED
@@ -524,6 +524,15 @@ export const STYLES = `
524
524
  floating circle that would escape the host's layout. */
525
525
  :host([collapsed]) {
526
526
  pointer-events: none;
527
+ /* A collapsed host has to be allowed to shrink, and in the layout hosts
528
+ actually use it is not. Every collapse path here works by letting the host
529
+ size to its content -- the in-flow ones set height: auto, the floating one
530
+ leaves only the launcher -- and a flex or grid parent whose align-items is
531
+ the default stretch value overrides all of it. The panel then hides and the box
532
+ it occupied stays: a header bar over several hundred pixels of nothing.
533
+ Every known consumer hit this, because putting the element in a flex column
534
+ beside the page content is the obvious way to embed it. */
535
+ align-self: start;
527
536
  }
528
537
 
529
538
  :host([collapsed]) .chat {
@@ -82,6 +82,18 @@ export class SubAgentPanel {
82
82
  /** The panel's root; append this into the delegating card's slot. */
83
83
  readonly element: HTMLDivElement;
84
84
 
85
+ /**
86
+ * The child agent's name, as the last update that carried one gave it.
87
+ *
88
+ * Read back rather than only written to the DOM, because the two carriers
89
+ * split the name away from the close: `SUBAGENT_STARTED` names the agent and
90
+ * `SUBAGENT_FINISHED` carries only an id, so whoever words the closing line
91
+ * has to recover the name from what the delegation already told it.
92
+ */
93
+ get agent(): string | null {
94
+ return this.#agent;
95
+ }
96
+
85
97
  /**
86
98
  * The collapsed row, which is the expander as well as the status.
87
99
  *
@@ -94,6 +106,7 @@ export class SubAgentPanel {
94
106
  readonly #steps: HTMLDivElement;
95
107
  /** The child's tool calls, keyed by the child's own call id. */
96
108
  readonly #stepRows = new Map<string, HTMLDivElement>();
109
+ #agent: string | null = null;
97
110
 
98
111
  constructor(strings: UiStrings = DEFAULT_UI_STRINGS) {
99
112
  this.element = document.createElement("div");
@@ -151,6 +164,7 @@ export class SubAgentPanel {
151
164
  report(update: SubAgentUpdate): void {
152
165
  this.element.setAttribute("data-phase", update.phase);
153
166
  if (update.agent !== null) {
167
+ this.#agent = update.agent;
154
168
  this.element.setAttribute("data-agent", update.agent);
155
169
  }
156
170
  if (update.status !== null) {
@@ -5,6 +5,15 @@
5
5
  * renderer: the panel's job is drawing, and a value that reaches it has already
6
6
  * been vouched for.
7
7
  *
8
+ * **The three lifecycle phases are still accepted here, deliberately.** A current
9
+ * server sends only `tool_call` and `tool_result` on this carrier -- the
10
+ * delegation's lifetime moved to the protocol's `SUBAGENT_*` events -- but a
11
+ * server one release older sends all five, and this element is published and
12
+ * vendored separately from it. Continuing to narrow the older shape costs
13
+ * nothing (the panel needs those phases as visual states regardless) and is the
14
+ * difference between a mixed-version pair that degrades and one that shows a
15
+ * delegation which never opens.
16
+ *
8
17
  * Defensive about the payload, not about the name. A `CUSTOM` event's `value` is
9
18
  * `unknown` by the protocol, so a server can put anything there, and a malformed
10
19
  * announcement must not take a run down with it — the same rule the invalidation
@@ -140,6 +140,25 @@ export interface UiStrings {
140
140
  * must never be blank.
141
141
  */
142
142
  subAgentWorking: string;
143
+ /**
144
+ * The row's line while a delegation is running. Token: `{agent}`.
145
+ *
146
+ * Worded here rather than on the wire, unlike the two step phases. The
147
+ * protocol's `SUBAGENT_STARTED` carries the sub-agent's `name` and no rendered
148
+ * status -- which is the better shape for a localised UI, and the reason this
149
+ * string exists at all.
150
+ */
151
+ subAgentDelegatedTo: string;
152
+ /** The row's line once a delegation completed. Token: `{agent}`. */
153
+ subAgentFinished: string;
154
+ /**
155
+ * The row's line for a delegation that failed and named no agent.
156
+ *
157
+ * A fallback only: `SUBAGENT_ERROR` carries a required `message`, which the
158
+ * server fills with the sub-agent's name and nothing else, and that text is
159
+ * what the row shows. This covers a server that sent an empty one.
160
+ */
161
+ subAgentFailed: string;
143
162
  /** `aria-label` of the region holding the sub-agent's own tool calls. */
144
163
  subAgentSteps: string;
145
164
 
@@ -332,6 +351,9 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
332
351
  details: "Details",
333
352
 
334
353
  subAgentWorking: "Working…",
354
+ subAgentDelegatedTo: "Delegated to {agent}",
355
+ subAgentFinished: "{agent} finished",
356
+ subAgentFailed: "The sub-agent failed",
335
357
  subAgentSteps: "Steps the sub-agent took",
336
358
 
337
359
  approvalEditArgs: "Edit the arguments before approving",
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.31.0";
1
+ export const VERSION: string = "0.32.0";