@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/CHANGELOG.md +112 -1
- package/README.md +53 -5
- package/dist/ag-ui-web-component.bundle.js +37 -37
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +25 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +25 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/tool_outcome.d.ts +27 -0
- package/dist/core/tool_outcome.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +174 -44
- package/dist/index.js.map +3 -3
- package/dist/ui/ui_strings.d.ts +4 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +26 -0
- package/src/core/ag_ui_chat.ts +148 -39
- package/src/core/agui_client.ts +105 -8
- package/src/core/tool_outcome.ts +36 -0
- package/src/index.ts +2 -0
- package/src/ui/ui_strings.ts +6 -0
- package/src/version.ts +1 -1
package/dist/ui/ui_strings.d.ts
CHANGED
|
@@ -89,6 +89,10 @@ export interface UiStrings {
|
|
|
89
89
|
pageMoved: string;
|
|
90
90
|
/** Notice when Send ran while a file was still uploading. Token: `{n}`. */
|
|
91
91
|
attachmentsStillUploading: string;
|
|
92
|
+
/** Notice shown when a message was typed at an element with no `endpoint`. */
|
|
93
|
+
notConnected: string;
|
|
94
|
+
/** Composer hint when a run continuation was picked with an empty composer. */
|
|
95
|
+
continueNeedsTurn: string;
|
|
92
96
|
/** `aria-label` of the message textarea. */
|
|
93
97
|
message: string;
|
|
94
98
|
/** Placeholder of the message textarea. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui_strings.d.ts","sourceRoot":"","sources":["../../src/ui/ui_strings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,SAAS;IAExB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IAGpB,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,wBAAwB,EAAE,MAAM,CAAC;IACjC,mEAAmE;IACnE,eAAe,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,cAAc,EAAE,MAAM,CAAC;IACvB;oDACgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,cAAc,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,cAAc,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,gBAAgB,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,cAAc,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,yBAAyB,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ui_strings.d.ts","sourceRoot":"","sources":["../../src/ui/ui_strings.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.
|
|
3
|
+
"version": "0.37.0",
|
|
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",
|
package/src/constants.ts
CHANGED
|
@@ -263,6 +263,32 @@ export const TOOL_CALL_STATUS = {
|
|
|
263
263
|
DECLINED: "declined",
|
|
264
264
|
} as const;
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* How a tool call ended, as the server states it on `TOOL_CALL_RESULT`.
|
|
268
|
+
*
|
|
269
|
+
* The vocabulary is pydantic-ai's own `ToolReturnPart.outcome`, carried whole
|
|
270
|
+
* rather than re-spelled, so the four repos that pass a refusal along agree on
|
|
271
|
+
* one word for it.
|
|
272
|
+
*
|
|
273
|
+
* **Absent means {@link TOOL_OUTCOME.SUCCESS}, and that is load-bearing.** Every
|
|
274
|
+
* server written before the field existed omits it, so a missing field has to
|
|
275
|
+
* render exactly as a plain result did — which is why this is an optional
|
|
276
|
+
* annotation on the event and not a required one. `FAILED` is a call that ran
|
|
277
|
+
* and failed; `DENIED` is one a person or a guard refused, so it never ran at
|
|
278
|
+
* all. The two are worth distinguishing on screen because only the second is
|
|
279
|
+
* something the user did.
|
|
280
|
+
*
|
|
281
|
+
* Anything *else* on the wire — a value from a later protocol version, or
|
|
282
|
+
* pydantic-ai's own `interrupted` — is read as a success rather than rejected.
|
|
283
|
+
* A card is a claim about what happened, and "I do not know this word" is not
|
|
284
|
+
* grounds for claiming failure. `toolStatusFromOutcome` is where that is done.
|
|
285
|
+
*/
|
|
286
|
+
export const TOOL_OUTCOME = {
|
|
287
|
+
SUCCESS: "success",
|
|
288
|
+
FAILED: "failed",
|
|
289
|
+
DENIED: "denied",
|
|
290
|
+
} as const;
|
|
291
|
+
|
|
266
292
|
/**
|
|
267
293
|
* Lifecycle status of a pending-attachment chip in the composer tray. A chip
|
|
268
294
|
* opens as `UPLOADING` (with a progress bar), then settles to `READY` (a durable
|
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
TOGGLE_EVENT,
|
|
36
36
|
TOOL_CALL_STATUS,
|
|
37
37
|
TOOL_DISPLAY,
|
|
38
|
+
TOOL_OUTCOME,
|
|
38
39
|
UNREAD_EVENT,
|
|
39
40
|
X_CONFIRM_KEY,
|
|
40
41
|
X_SUMMARY_KEY,
|
|
@@ -144,6 +145,7 @@ import {
|
|
|
144
145
|
import { type AgentFactory, createHttpAgent } from "./create_http_agent.js";
|
|
145
146
|
import { RemoteConversationStore } from "./remote_conversation_store.js";
|
|
146
147
|
import { RunIndex } from "./run_index.js";
|
|
148
|
+
import { toolStatusFromOutcome } from "./tool_outcome.js";
|
|
147
149
|
import { type TranscribeHandler, transcribeAudio } from "./transcribe_audio.js";
|
|
148
150
|
import { type UploadHandler, uploadAttachment } from "./upload_attachment.js";
|
|
149
151
|
import { mintThread, warnOnCrossOriginCredentials, withCredentials } from "./utils.js";
|
|
@@ -827,7 +829,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
827
829
|
readonly #checkpoints: CheckpointMenu;
|
|
828
830
|
/** Built lazily from `data-runs-url`; `null` when the host didn't opt in. */
|
|
829
831
|
#runIndex: RunIndex | null = null;
|
|
830
|
-
|
|
832
|
+
/**
|
|
833
|
+
* The one-line hint above the composer, cleared by the next keystroke.
|
|
834
|
+
*
|
|
835
|
+
* Two things write it -- a skill whose template is short of a field, and a
|
|
836
|
+
* run continuation picked with nothing typed -- and both say the same kind of
|
|
837
|
+
* thing: what the composer still needs before this can go. Its `part` stays
|
|
838
|
+
* `skill-hint`, which the skills feature named and the README documents;
|
|
839
|
+
* renaming a part is breaking, and a second hint element in the same slot
|
|
840
|
+
* would be worse than one whose name is a release older than its job.
|
|
841
|
+
*/
|
|
842
|
+
readonly #composerHint: HTMLDivElement;
|
|
831
843
|
/** File-picker button + hidden input + tray slot; the tray mounts on connect. */
|
|
832
844
|
readonly #attachButton: HTMLButtonElement;
|
|
833
845
|
readonly #fileInput: HTMLInputElement;
|
|
@@ -1050,7 +1062,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
1050
1062
|
this.#input = document.createElement("textarea");
|
|
1051
1063
|
this.#send = document.createElement("button");
|
|
1052
1064
|
this.#title = document.createElement("span");
|
|
1053
|
-
this.#
|
|
1065
|
+
this.#composerHint = document.createElement("div");
|
|
1054
1066
|
this.#attachButton = document.createElement("button");
|
|
1055
1067
|
this.#fileInput = document.createElement("input");
|
|
1056
1068
|
this.#attachSlot = document.createElement("div");
|
|
@@ -1142,10 +1154,29 @@ export class AgUiChat extends HTMLElement {
|
|
|
1142
1154
|
async #continueRun(runId: string, verb: CheckpointVerb): Promise<void> {
|
|
1143
1155
|
const index = this.#runs();
|
|
1144
1156
|
if (index === null) {
|
|
1157
|
+
// Unreachable from the built-in control: the header button is only
|
|
1158
|
+
// rendered when `#runs()` is configured, so a row to pick cannot exist
|
|
1159
|
+
// without one. A host calling `openCheckpoints()` regardless gets the
|
|
1160
|
+
// documented empty panel, which has no rows either. Typed, not silent.
|
|
1145
1161
|
return;
|
|
1146
1162
|
}
|
|
1147
1163
|
const content = this.#input.value.trim();
|
|
1148
1164
|
if (content === "") {
|
|
1165
|
+
// A continuation sends *only* the next turn -- the snapshot supplies
|
|
1166
|
+
// everything before it -- so with an empty composer there is nothing to
|
|
1167
|
+
// send. Returning here was the same failure the endpoint guard above had:
|
|
1168
|
+
// the row's button closes the panel before this runs, so the widget
|
|
1169
|
+
// visibly reacted and then did nothing, which reads as a resume that was
|
|
1170
|
+
// attempted and lost rather than one that never started.
|
|
1171
|
+
//
|
|
1172
|
+
// Said at the composer rather than in the transcript, because that is
|
|
1173
|
+
// where the fix goes and because the hint clears itself on the first
|
|
1174
|
+
// keystroke -- a transcript notice for a recoverable slip would outlive
|
|
1175
|
+
// the slip. Focus follows for the same reason `#applySkill` moves it when
|
|
1176
|
+
// a template is short of a field.
|
|
1177
|
+
this.#composerHint.textContent = this.#strings.continueNeedsTurn;
|
|
1178
|
+
this.#composerHint.hidden = false;
|
|
1179
|
+
this.#input.focus();
|
|
1149
1180
|
return;
|
|
1150
1181
|
}
|
|
1151
1182
|
this.#input.value = "";
|
|
@@ -1803,21 +1834,45 @@ export class AgUiChat extends HTMLElement {
|
|
|
1803
1834
|
return value !== null && value !== "false";
|
|
1804
1835
|
}
|
|
1805
1836
|
|
|
1806
|
-
/**
|
|
1807
|
-
|
|
1808
|
-
|
|
1837
|
+
/**
|
|
1838
|
+
* Parse a JSON-valued attribute, saying so when it will not parse.
|
|
1839
|
+
*
|
|
1840
|
+
* `null` for an absent attribute, and `null` again for one that is not JSON --
|
|
1841
|
+
* but not quietly the second time. Quoting JSON inside an HTML attribute is
|
|
1842
|
+
* fiddly, and the result of getting it wrong is indistinguishable from the
|
|
1843
|
+
* feature being switched off: no chips appear, or the strings stay English,
|
|
1844
|
+
* with nothing anywhere saying why. That is the same failure `data-paste-attach`
|
|
1845
|
+
* already reports for a value it cannot read.
|
|
1846
|
+
*
|
|
1847
|
+
* Console only. A page author's typo is not the reader's business, nothing the
|
|
1848
|
+
* reader did was refused, and the degraded widget is still perfectly usable.
|
|
1849
|
+
*/
|
|
1850
|
+
#readJsonAttribute(name: string): unknown {
|
|
1851
|
+
const raw = this.getAttribute(name);
|
|
1809
1852
|
if (raw === null) {
|
|
1810
|
-
return
|
|
1853
|
+
return null;
|
|
1811
1854
|
}
|
|
1812
1855
|
try {
|
|
1813
|
-
|
|
1814
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
1815
|
-
return parsed as Partial<UiStrings>;
|
|
1816
|
-
}
|
|
1856
|
+
return JSON.parse(raw);
|
|
1817
1857
|
} catch {
|
|
1818
|
-
|
|
1858
|
+
console.warn(
|
|
1859
|
+
`<ag-ui-chat>: ${name} is not valid JSON, so it was ignored entirely and ` +
|
|
1860
|
+
"the built-in default is being used. Check the quoting -- JSON inside " +
|
|
1861
|
+
"an HTML attribute needs single quotes around the attribute value, or " +
|
|
1862
|
+
"its own double quotes escaped.",
|
|
1863
|
+
);
|
|
1864
|
+
return null;
|
|
1819
1865
|
}
|
|
1820
|
-
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
|
|
1869
|
+
#readStringOverrides(): Partial<UiStrings> {
|
|
1870
|
+
const parsed = this.#readJsonAttribute("data-strings");
|
|
1871
|
+
// A JSON number or string parses fine and overrides nothing. Not warned
|
|
1872
|
+
// about separately: it is the same "this attribute did not take effect"
|
|
1873
|
+
// as a parse failure, and the warning above already covers the spelling
|
|
1874
|
+
// that produces it by accident.
|
|
1875
|
+
return typeof parsed === "object" && parsed !== null ? (parsed as Partial<UiStrings>) : {};
|
|
1821
1876
|
}
|
|
1822
1877
|
|
|
1823
1878
|
/**
|
|
@@ -2276,15 +2331,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
2276
2331
|
|
|
2277
2332
|
/** Parse the inline `data-skills` JSON catalog (empty when absent/malformed). */
|
|
2278
2333
|
#readEmbeddedSkills(): readonly Skill[] {
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
}
|
|
2283
|
-
try {
|
|
2284
|
-
return parseSkills(JSON.parse(raw));
|
|
2285
|
-
} catch {
|
|
2286
|
-
return [];
|
|
2287
|
-
}
|
|
2334
|
+
// `parseSkills` drops anything that is not a well-formed skill, `null`
|
|
2335
|
+
// included, so the absent and unparseable cases need no branch here.
|
|
2336
|
+
return parseSkills(this.#readJsonAttribute("data-skills"));
|
|
2288
2337
|
}
|
|
2289
2338
|
|
|
2290
2339
|
/** Fetch the backend skills catalog from `data-skills-url`, if set. */
|
|
@@ -2327,7 +2376,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
2327
2376
|
*/
|
|
2328
2377
|
#applySkill(skill: Skill): void {
|
|
2329
2378
|
if (skill.prompt === undefined) {
|
|
2330
|
-
this.#
|
|
2379
|
+
this.#composerHint.hidden = true;
|
|
2331
2380
|
void this.sendMessage(`/${skill.name}`);
|
|
2332
2381
|
return;
|
|
2333
2382
|
}
|
|
@@ -2339,17 +2388,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
2339
2388
|
// keystroke replaces it. Blocking with a hint alone left whatever the
|
|
2340
2389
|
// user had typed to open the palette — a lone "/" — sitting there, which
|
|
2341
2390
|
// says nothing about what the skill wanted or how to give it.
|
|
2342
|
-
this.#
|
|
2391
|
+
this.#composerHint.textContent = this.#strings.skillNeeds
|
|
2343
2392
|
.replace("{title}", skill.title)
|
|
2344
2393
|
.replace("{fields}", missing.join(", "));
|
|
2345
|
-
this.#
|
|
2394
|
+
this.#composerHint.hidden = false;
|
|
2346
2395
|
this.#input.value = text;
|
|
2347
2396
|
this.#autoGrow();
|
|
2348
2397
|
this.#input.focus();
|
|
2349
2398
|
this.#selectFirstPlaceholder(text);
|
|
2350
2399
|
return;
|
|
2351
2400
|
}
|
|
2352
|
-
this.#
|
|
2401
|
+
this.#composerHint.hidden = true;
|
|
2353
2402
|
this.#input.value = text;
|
|
2354
2403
|
this.#autoGrow();
|
|
2355
2404
|
if (skill.sendImmediately === false) {
|
|
@@ -3995,7 +4044,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
3995
4044
|
if (message.role === "tool") {
|
|
3996
4045
|
const card = this.#toolCards.get(message.toolCallId);
|
|
3997
4046
|
if (card !== undefined) {
|
|
3998
|
-
|
|
4047
|
+
// The outcome `AgUiClient` annotated onto the persisted message, read
|
|
4048
|
+
// back through the same mapping the live path uses -- so a card that
|
|
4049
|
+
// said "declined" before the reload still says it after. Narrowed off
|
|
4050
|
+
// `unknown` rather than trusted, like every other field read out of the
|
|
4051
|
+
// store: `Message` does not declare it, a host store may not round-trip
|
|
4052
|
+
// it, and history written before this shipped has none. All three land
|
|
4053
|
+
// on DONE, which is what this line did unconditionally.
|
|
4054
|
+
card.settle(
|
|
4055
|
+
toolStatusFromOutcome((message as { outcome?: unknown }).outcome),
|
|
4056
|
+
message.content,
|
|
4057
|
+
);
|
|
3999
4058
|
}
|
|
4000
4059
|
}
|
|
4001
4060
|
}
|
|
@@ -4306,9 +4365,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
4306
4365
|
void this.#submit();
|
|
4307
4366
|
});
|
|
4308
4367
|
|
|
4309
|
-
this.#
|
|
4310
|
-
this.#
|
|
4311
|
-
this.#
|
|
4368
|
+
this.#composerHint.className = "skill-hint";
|
|
4369
|
+
this.#composerHint.setAttribute("part", "skill-hint");
|
|
4370
|
+
this.#composerHint.hidden = true;
|
|
4312
4371
|
|
|
4313
4372
|
// File-upload affordance: a paperclip button (hidden until
|
|
4314
4373
|
// `data-attachments-url` is wired) opening a hidden multi-file input.
|
|
@@ -4340,8 +4399,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
4340
4399
|
tools.append(this.#attachButton, this.#voiceSlot, this.#send);
|
|
4341
4400
|
composer.append(this.#input, tools);
|
|
4342
4401
|
inputRow.append(composer, this.#fileInput);
|
|
4343
|
-
//
|
|
4344
|
-
// the
|
|
4402
|
+
// Composer surfaces sit just above the input: the skills palette (opens on
|
|
4403
|
+
// `/`), the chips, the hint saying what the composer still needs, and the
|
|
4404
|
+
// pending-attachments tray.
|
|
4345
4405
|
this.#messagesWrap.className = "messages-wrap";
|
|
4346
4406
|
// Sibling of the list inside a shared box, not a child of it: the
|
|
4347
4407
|
// affordance offering to scroll must not scroll away with the content.
|
|
@@ -4352,7 +4412,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
4352
4412
|
this.#messagesWrap,
|
|
4353
4413
|
this.#skillsMenu.palette,
|
|
4354
4414
|
this.#skillsMenu.chips,
|
|
4355
|
-
this.#
|
|
4415
|
+
this.#composerHint,
|
|
4356
4416
|
this.#queuedRow,
|
|
4357
4417
|
this.#attachSlot,
|
|
4358
4418
|
inputRow,
|
|
@@ -4677,10 +4737,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
4677
4737
|
this.#emptyWrap.hidden = this.#messages.childElementCount > 1;
|
|
4678
4738
|
}
|
|
4679
4739
|
|
|
4680
|
-
/**
|
|
4740
|
+
/**
|
|
4741
|
+
* Forward input changes to the skills palette and clear any stale hint.
|
|
4742
|
+
*
|
|
4743
|
+
* Typing is the answer to every hint that surface carries -- a skill short of
|
|
4744
|
+
* a field, a continuation short of its next turn -- so the keystroke that
|
|
4745
|
+
* starts answering it is the right moment to take it down.
|
|
4746
|
+
*/
|
|
4681
4747
|
#onInput(): void {
|
|
4682
4748
|
this.#skillsMenu.onInput(this.#input.value);
|
|
4683
|
-
this.#
|
|
4749
|
+
this.#composerHint.hidden = true;
|
|
4684
4750
|
this.#autoGrow();
|
|
4685
4751
|
// Typing puts the composer back in the user's hands: the next ArrowUp
|
|
4686
4752
|
// starts from the newest turn again rather than continuing a walk through
|
|
@@ -4980,8 +5046,39 @@ export class AgUiChat extends HTMLElement {
|
|
|
4980
5046
|
);
|
|
4981
5047
|
}
|
|
4982
5048
|
|
|
5049
|
+
/**
|
|
5050
|
+
* Hand the message to the client, or say why it is going nowhere.
|
|
5051
|
+
*
|
|
5052
|
+
* With no `endpoint` there is nothing to send to, and this used to return
|
|
5053
|
+
* here in silence. That is the worst shape the failure can take, because the
|
|
5054
|
+
* two halves of a send that *did* work have already happened by now: the
|
|
5055
|
+
* user's bubble is on screen and {@link SUBMIT_EVENT} has been dispatched. So
|
|
5056
|
+
* the message looks sent, the composer is empty, the Send button never turns
|
|
5057
|
+
* into Stop, and no request is ever made -- an unanswered question rather
|
|
5058
|
+
* than a broken widget. Nothing reached the console either, so the developer
|
|
5059
|
+
* had no thread to pull and the user had no reason to think anything was
|
|
5060
|
+
* wrong with the page.
|
|
5061
|
+
*
|
|
5062
|
+
* Both audiences are told, because they need different things. The console
|
|
5063
|
+
* carries the developer's version -- the attribute is missing, here is what
|
|
5064
|
+
* to set -- since a missing attribute is a page's mistake and not the
|
|
5065
|
+
* reader's. The transcript carries the reader's, because they are looking at
|
|
5066
|
+
* their own message waiting for a reply that cannot come, and the honest
|
|
5067
|
+
* alternative to one muted line is an indefinite wait.
|
|
5068
|
+
*
|
|
5069
|
+
* Said on every attempt rather than once. Each send is a separate thing the
|
|
5070
|
+
* user asked for and did not get, and a once-only report is silence for every
|
|
5071
|
+
* attempt after the first -- which is the defect again, just later.
|
|
5072
|
+
*/
|
|
4983
5073
|
async #client_send(content: string, attachments: readonly AttachmentRef[]): Promise<void> {
|
|
4984
5074
|
if (this.endpoint === "") {
|
|
5075
|
+
console.error(
|
|
5076
|
+
"<ag-ui-chat>: no endpoint is set, so this message was not sent and no " +
|
|
5077
|
+
"request was made. Point the element at your AG-UI mount with the " +
|
|
5078
|
+
'endpoint attribute (endpoint="/agent/"), or assign chat.endpoint ' +
|
|
5079
|
+
"before sending.",
|
|
5080
|
+
);
|
|
5081
|
+
this.#appendNotice("⚠", this.#strings.notConnected, "not-connected");
|
|
4985
5082
|
return;
|
|
4986
5083
|
}
|
|
4987
5084
|
await this.#ensureClient().send(content, attachments);
|
|
@@ -5175,7 +5272,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
5175
5272
|
const message = this.#strings.pageMoved;
|
|
5176
5273
|
card.settle(TOOL_CALL_STATUS.ERROR, message);
|
|
5177
5274
|
this.#showPending();
|
|
5178
|
-
|
|
5275
|
+
// Stated so a reload settles this card the same way. The card's own status
|
|
5276
|
+
// lives only in the DOM, and the DOM is what a reload throws away.
|
|
5277
|
+
return { content: `Error: ${message}`, error: message, outcome: TOOL_OUTCOME.FAILED };
|
|
5179
5278
|
}
|
|
5180
5279
|
const rule = await this.#confirmationRule(call, tool);
|
|
5181
5280
|
if (rule !== null) {
|
|
@@ -5208,7 +5307,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
5208
5307
|
const message = this.#strings.declinedAction;
|
|
5209
5308
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
5210
5309
|
this.#showPending();
|
|
5211
|
-
|
|
5310
|
+
// The one outcome with no error text and no server involvement at all:
|
|
5311
|
+
// a person said no in this browser. Nothing else records that, so
|
|
5312
|
+
// without the annotation the reload showed a green card for an action
|
|
5313
|
+
// the user had explicitly refused.
|
|
5314
|
+
return { content: message, outcome: TOOL_OUTCOME.DENIED };
|
|
5212
5315
|
}
|
|
5213
5316
|
}
|
|
5214
5317
|
// A navigating tool reloads only without a client-side router; with a
|
|
@@ -5251,7 +5354,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
5251
5354
|
const message = error instanceof Error ? error.message : String(error);
|
|
5252
5355
|
card.settle(TOOL_CALL_STATUS.ERROR, message);
|
|
5253
5356
|
this.#showPending();
|
|
5254
|
-
return { content: `Error: ${message}`, error: message };
|
|
5357
|
+
return { content: `Error: ${message}`, error: message, outcome: TOOL_OUTCOME.FAILED };
|
|
5255
5358
|
}
|
|
5256
5359
|
}
|
|
5257
5360
|
|
|
@@ -5516,12 +5619,18 @@ export class AgUiChat extends HTMLElement {
|
|
|
5516
5619
|
// got for compaction, one handler up.
|
|
5517
5620
|
this.#appendNotice("\u{1F504}", this.#strings.historyReplaced, "history-replaced");
|
|
5518
5621
|
},
|
|
5519
|
-
onToolResult: (toolCallId, content) => {
|
|
5622
|
+
onToolResult: (toolCallId, content, outcome) => {
|
|
5520
5623
|
const card = this.#toolCards.get(toolCallId);
|
|
5521
5624
|
if (card === undefined) {
|
|
5522
5625
|
return;
|
|
5523
5626
|
}
|
|
5524
|
-
|
|
5627
|
+
// Settled as the server says it ended, not as "it ended". This path used
|
|
5628
|
+
// to pass DONE unconditionally, so a refusal arrived as a green card
|
|
5629
|
+
// with the reason folded inside it -- a booking the server declined
|
|
5630
|
+
// read, at a glance, as a booking that was made. An absent or
|
|
5631
|
+
// unrecognised outcome still means DONE, so every server written before
|
|
5632
|
+
// the field existed renders exactly as it did.
|
|
5633
|
+
card.settle(toolStatusFromOutcome(outcome), content);
|
|
5525
5634
|
this.#serverSettled.add(toolCallId);
|
|
5526
5635
|
// The card stops being the live thing the moment it settles, and the
|
|
5527
5636
|
// server goes straight back to the model with the result -- a wait with
|
package/src/core/agui_client.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import type { Context, Interrupt, Message, ResumeEntry, Tool } from "@ag-ui/core";
|
|
9
9
|
import { MAX_TOOL_ROUNDS } from "../constants.js";
|
|
10
10
|
import type { AttachmentRef } from "./attachment.js";
|
|
11
|
+
import type { ToolOutcome } from "./tool_outcome.js";
|
|
11
12
|
|
|
12
13
|
/** A tool call surfaced to the host by {@link AgUiClient}. */
|
|
13
14
|
export interface AgUiToolCall {
|
|
@@ -22,6 +23,18 @@ export interface ToolExecution {
|
|
|
22
23
|
content: string;
|
|
23
24
|
/** Present when the handler failed; surfaced for logging. */
|
|
24
25
|
error?: string;
|
|
26
|
+
/**
|
|
27
|
+
* How the call ended, in the same vocabulary a server states on
|
|
28
|
+
* `TOOL_CALL_RESULT`. Omit for a success -- absent *is* success, here for the
|
|
29
|
+
* same reason it is on the wire.
|
|
30
|
+
*
|
|
31
|
+
* Distinct from {@link error}, which is a *message* and only exists for the
|
|
32
|
+
* one failure shape that produces one. A refusal has no error and is still
|
|
33
|
+
* not a success, and that is the case this field exists for: without it a
|
|
34
|
+
* declined call was persisted as an ordinary result and came back from a
|
|
35
|
+
* reload as a green card.
|
|
36
|
+
*/
|
|
37
|
+
outcome?: ToolOutcome;
|
|
25
38
|
/**
|
|
26
39
|
* When `true`, a navigating tool triggered a page reload. The loop stops
|
|
27
40
|
* without appending a result — the result is supplied after the next mount
|
|
@@ -76,8 +89,19 @@ export interface AgUiClientHandlers {
|
|
|
76
89
|
* Fired when a server-side tool's result streams back (AG-UI's
|
|
77
90
|
* `TOOL_CALL_RESULT`). Frontend tools don't emit this — the client supplies
|
|
78
91
|
* their result itself — so this is the channel for server-executed output.
|
|
92
|
+
*
|
|
93
|
+
* `outcome` is the event's optional `outcome` field, forwarded raw. It is
|
|
94
|
+
* `unknown` rather than {@link ToolOutcome} because it comes off a
|
|
95
|
+
* `passthrough` zod schema: the protocol does not validate it, so neither can
|
|
96
|
+
* this signature honestly claim to. Read it with `toolStatusFromOutcome`,
|
|
97
|
+
* which treats `undefined` and anything unrecognised as a success.
|
|
98
|
+
*
|
|
99
|
+
* Added as a third parameter rather than as a new callback, so an
|
|
100
|
+
* implementation written against the two-parameter form still satisfies this
|
|
101
|
+
* interface and still behaves exactly as it did — which is the same
|
|
102
|
+
* backwards-compatibility promise the wire field makes.
|
|
79
103
|
*/
|
|
80
|
-
onToolResult(toolCallId: string, content: string): void;
|
|
104
|
+
onToolResult(toolCallId: string, content: string, outcome?: unknown): void;
|
|
81
105
|
/**
|
|
82
106
|
* Fired for AG-UI activity events — ambient notices about what the *run* did,
|
|
83
107
|
* as opposed to work the agent asked for. `django-ag-ui` emits one with
|
|
@@ -259,6 +283,25 @@ export class AgUiClient {
|
|
|
259
283
|
* set would miss entirely.
|
|
260
284
|
*/
|
|
261
285
|
readonly #closedMessageIds = new Set<string>();
|
|
286
|
+
/**
|
|
287
|
+
* How each tool call ended, keyed by call id, for the transcript this client
|
|
288
|
+
* persists.
|
|
289
|
+
*
|
|
290
|
+
* A side table rather than a field written onto the message, because neither
|
|
291
|
+
* producer of a tool message will carry it. `@ag-ui/client` builds the
|
|
292
|
+
* server-side one by destructuring five named fields off the event, so an
|
|
293
|
+
* `outcome` beside them is dropped before the message exists; and writing it
|
|
294
|
+
* back onto that message afterwards would put it in `agent.messages`, which is
|
|
295
|
+
* what the *next* request sends to the server. This keeps the annotation on
|
|
296
|
+
* the copy handed to the store and off the wire.
|
|
297
|
+
*
|
|
298
|
+
* Per client, not per run: `saveMessages` rewrites the whole transcript on
|
|
299
|
+
* every persist, so an outcome recorded in round one has to still be here in
|
|
300
|
+
* round five or the earlier card silently reverts to a green one. Bounded by
|
|
301
|
+
* the number of tool calls in the conversation, which the transcript beside it
|
|
302
|
+
* already is.
|
|
303
|
+
*/
|
|
304
|
+
readonly #outcomes = new Map<string, string>();
|
|
262
305
|
readonly #connectionLostMessage: string;
|
|
263
306
|
readonly #maxToolRounds: number;
|
|
264
307
|
// Set by cancel(); reset at the top of each #run(). Checked by the loop so
|
|
@@ -334,7 +377,7 @@ export class AgUiClient {
|
|
|
334
377
|
(message as { attachments?: readonly AttachmentRef[] }).attachments = attachments;
|
|
335
378
|
}
|
|
336
379
|
this.#agent.addMessage(message);
|
|
337
|
-
this.#
|
|
380
|
+
this.#persist();
|
|
338
381
|
await this.#run();
|
|
339
382
|
}
|
|
340
383
|
|
|
@@ -373,7 +416,7 @@ export class AgUiClient {
|
|
|
373
416
|
}
|
|
374
417
|
const kept = messages.slice(0, lastUser + 1);
|
|
375
418
|
this.#agent.setMessages(kept);
|
|
376
|
-
this.#
|
|
419
|
+
this.#persist();
|
|
377
420
|
return kept;
|
|
378
421
|
}
|
|
379
422
|
|
|
@@ -389,7 +432,7 @@ export class AgUiClient {
|
|
|
389
432
|
/** Append a frontend tool result to history (used by the resume path). */
|
|
390
433
|
addToolResult(toolCallId: string, content: string): void {
|
|
391
434
|
this.#agent.addMessage({ id: randomUUID(), role: "tool", content, toolCallId });
|
|
392
|
-
this.#
|
|
435
|
+
this.#persist();
|
|
393
436
|
}
|
|
394
437
|
|
|
395
438
|
/**
|
|
@@ -428,10 +471,42 @@ export class AgUiClient {
|
|
|
428
471
|
#onCancelled(): void {
|
|
429
472
|
// Persist so the truncated exchange, partial assistant text included,
|
|
430
473
|
// survives a reload.
|
|
431
|
-
this.#
|
|
474
|
+
this.#persist();
|
|
432
475
|
this.#handlers.onCancelled();
|
|
433
476
|
}
|
|
434
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Hand the transcript to the host's store, annotated with what {@link #outcomes}
|
|
480
|
+
* knows about how each tool call ended.
|
|
481
|
+
*
|
|
482
|
+
* Every persist in this class goes through here, because the store keeps only
|
|
483
|
+
* the most recent list: annotating one call site would mean the next
|
|
484
|
+
* unannotated save quietly threw the annotations away.
|
|
485
|
+
*/
|
|
486
|
+
#persist(): void {
|
|
487
|
+
const messages = this.#agent.messages;
|
|
488
|
+
if (this.#outcomes.size === 0) {
|
|
489
|
+
this.#onPersist(messages);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// A copy, and only of the messages that gain something. `agent.messages` is
|
|
493
|
+
// the list the next `runAgent` sends back to the server, so writing an extra
|
|
494
|
+
// field into it would put a client-side annotation on the wire; a store is
|
|
495
|
+
// 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
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
435
510
|
async #runLoop(): Promise<void> {
|
|
436
511
|
// Carries resolved approval answers into the next run when a round finished
|
|
437
512
|
// on a server-side-tool interrupt. Distinct from the public resume(), which
|
|
@@ -455,7 +530,7 @@ export class AgUiClient {
|
|
|
455
530
|
}
|
|
456
531
|
await this.#agent.runAgent(params, this.#buildSubscriber(pending, runState));
|
|
457
532
|
resume = undefined;
|
|
458
|
-
this.#
|
|
533
|
+
this.#persist();
|
|
459
534
|
// Cancelled mid-stream: don't execute the tool calls collected before the
|
|
460
535
|
// abort.
|
|
461
536
|
if (this.#cancelled) {
|
|
@@ -503,13 +578,21 @@ export class AgUiClient {
|
|
|
503
578
|
// next mount. Stop here rather than re-running into a dead context.
|
|
504
579
|
return;
|
|
505
580
|
}
|
|
581
|
+
// Recorded before the persist below, so the very first save of this
|
|
582
|
+
// message already carries how it ended. A frontend tool's refusal or
|
|
583
|
+
// failure never touches the wire's `outcome` field -- no server states
|
|
584
|
+
// it, because no server ran the call -- so this side table is the only
|
|
585
|
+
// record there is, and a reload reads a card off it.
|
|
586
|
+
if (result.outcome !== undefined) {
|
|
587
|
+
this.#outcomes.set(call.id, result.outcome);
|
|
588
|
+
}
|
|
506
589
|
this.#agent.addMessage({
|
|
507
590
|
id: randomUUID(),
|
|
508
591
|
role: "tool",
|
|
509
592
|
content: result.content,
|
|
510
593
|
toolCallId: call.id,
|
|
511
594
|
});
|
|
512
|
-
this.#
|
|
595
|
+
this.#persist();
|
|
513
596
|
executed = true;
|
|
514
597
|
}
|
|
515
598
|
if (!executed) {
|
|
@@ -521,6 +604,7 @@ export class AgUiClient {
|
|
|
521
604
|
#buildSubscriber(pending: AgUiToolCall[], runState: RunState): AgentSubscriber {
|
|
522
605
|
const h = this.#handlers;
|
|
523
606
|
const closed = this.#closedMessageIds;
|
|
607
|
+
const outcomes = this.#outcomes;
|
|
524
608
|
// Read at event time, not captured now: the flag flips mid-run, and the
|
|
525
609
|
// subscriber is built before the run that a later `cancel()` stops.
|
|
526
610
|
const cancelled = (): boolean => this.#cancelled;
|
|
@@ -562,7 +646,20 @@ export class AgUiClient {
|
|
|
562
646
|
h.onToolCall(call);
|
|
563
647
|
},
|
|
564
648
|
onToolCallResultEvent({ event }) {
|
|
565
|
-
|
|
649
|
+
// Bracket access because the field is not declared: `TOOL_CALL_RESULT`
|
|
650
|
+
// extends a `passthrough` schema, so an unknown key survives parsing and
|
|
651
|
+
// arrives here typed only by the catch-all index signature. That is the
|
|
652
|
+
// whole mechanism the outcome rides -- no schema change in `@ag-ui/core`
|
|
653
|
+
// is needed for a server to state one.
|
|
654
|
+
const outcome = event["outcome"];
|
|
655
|
+
// Recorded even when it is a word this client does not recognise, and
|
|
656
|
+
// even when it says "success": the store is a record of what the server
|
|
657
|
+
// said, and re-reading it through the same mapping as the live path is
|
|
658
|
+
// what keeps a reload agreeing with what the user watched happen.
|
|
659
|
+
if (typeof outcome === "string") {
|
|
660
|
+
outcomes.set(event.toolCallId, outcome);
|
|
661
|
+
}
|
|
662
|
+
h.onToolResult(event.toolCallId, event.content, outcome);
|
|
566
663
|
},
|
|
567
664
|
onActivitySnapshotEvent({ event, messages }) {
|
|
568
665
|
// A snapshot for an id already in the list is a replacement, not a new
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TOOL_CALL_STATUS, TOOL_OUTCOME } from "../constants.js";
|
|
2
|
+
import type { SettledStatus } from "../ui/tool_call_card.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How a tool call ended, in the wire's own words. See {@link TOOL_OUTCOME}.
|
|
6
|
+
*
|
|
7
|
+
* Absent is the fourth case and the common one: a server that has never heard
|
|
8
|
+
* of the field says nothing, and nothing means success.
|
|
9
|
+
*/
|
|
10
|
+
export type ToolOutcome = (typeof TOOL_OUTCOME)[keyof typeof TOOL_OUTCOME];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The card status a wire outcome settles into.
|
|
14
|
+
*
|
|
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.
|
|
19
|
+
*
|
|
20
|
+
* **Everything unrecognised maps to `DONE`.** Not because unknown values are
|
|
21
|
+
* expected to be successes, but because the alternative is worse in the
|
|
22
|
+
* direction that matters: a card claiming a call failed when it did not is a
|
|
23
|
+
* lie the user acts on, while a card claiming success has at least the result
|
|
24
|
+
* text under it for them to read. `interrupted` is the concrete case today --
|
|
25
|
+
* pydantic-ai emits it, this vocabulary does not carry it, and a future release
|
|
26
|
+
* may add more. Forward compatibility is the point of the open field.
|
|
27
|
+
*/
|
|
28
|
+
export function toolStatusFromOutcome(outcome: unknown): SettledStatus {
|
|
29
|
+
if (outcome === TOOL_OUTCOME.FAILED) {
|
|
30
|
+
return TOOL_CALL_STATUS.ERROR;
|
|
31
|
+
}
|
|
32
|
+
if (outcome === TOOL_OUTCOME.DENIED) {
|
|
33
|
+
return TOOL_CALL_STATUS.DECLINED;
|
|
34
|
+
}
|
|
35
|
+
return TOOL_CALL_STATUS.DONE;
|
|
36
|
+
}
|