@artooi/ag-ui-web-component 0.26.0 → 0.27.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 +95 -1
- package/README.md +29 -6
- package/dist/ag-ui-web-component.bundle.js +18 -18
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/core/ag_ui_chat.d.ts +6 -2
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +15 -2
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +6 -0
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/utils.d.ts +14 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.js +75 -33
- package/dist/index.js.map +3 -3
- package/dist/tools/client_tool_registry.d.ts +10 -1
- package/dist/tools/client_tool_registry.d.ts.map +1 -1
- package/dist/ui/chart_block.d.ts.map +1 -1
- package/dist/ui/chart_spec_from.d.ts.map +1 -1
- package/dist/ui/chart_tool.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +48 -19
- package/src/core/agui_client.ts +25 -1
- package/src/core/conversation_store.ts +19 -8
- package/src/core/remote_conversation_store.ts +10 -1
- package/src/core/utils.ts +25 -1
- package/src/tools/client_tool_registry.ts +11 -1
- package/src/ui/chart_block.ts +6 -1
- package/src/ui/chart_spec_from.ts +21 -2
- package/src/ui/chart_tool.ts +13 -5
- package/src/version.ts +1 -1
|
@@ -6,6 +6,15 @@ import type { Tool } from "@ag-ui/core";
|
|
|
6
6
|
* `handler` receives the parsed arguments and returns a result that is
|
|
7
7
|
* JSON-serialised into the AG-UI tool-result message sent back to the agent.
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Draws one call from its arguments alone.
|
|
11
|
+
*
|
|
12
|
+
* Named separately so the replay path can take *this* and never the tool that
|
|
13
|
+
* owns it: a function of this type cannot reach a `handler`, which is what
|
|
14
|
+
* makes "a reload never re-runs a tool's effect" a property of the code rather
|
|
15
|
+
* than a note asking maintainers to be careful.
|
|
16
|
+
*/
|
|
17
|
+
export type ChartRenderer = (args: Record<string, unknown>) => Node | null;
|
|
9
18
|
export interface ClientTool {
|
|
10
19
|
name: string;
|
|
11
20
|
description: string;
|
|
@@ -33,7 +42,7 @@ export interface ClientTool {
|
|
|
33
42
|
*
|
|
34
43
|
* Return `null` for arguments that say nothing worth drawing.
|
|
35
44
|
*/
|
|
36
|
-
render?:
|
|
45
|
+
render?: ChartRenderer;
|
|
37
46
|
}
|
|
38
47
|
/**
|
|
39
48
|
* Holds the frontend tools a host has declared on an `<ag-ui-chat>` element.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client_tool_registry.d.ts","sourceRoot":"","sources":["../../src/tools/client_tool_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxF;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"client_tool_registry.d.ts","sourceRoot":"","sources":["../../src/tools/client_tool_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;GAMG;AACH;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,IAAI,CAAC;AAE3E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxF;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED;;;;;;GAMG;AACH,qBAAa,kBAAkB;;IAG7B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAE/B;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzB;IAED,yCAAyC;IACzC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAM5B;IAED,wDAAwD;IACxD,KAAK,IAAI,IAAI,EAAE,CAMd;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart_block.d.ts","sourceRoot":"","sources":["../../src/ui/chart_block.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,mCAAmC;AACnC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,2BAA2B;AAC3B,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvE,wBAAwB;AACxB,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;CACzC;AAoBD,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD;
|
|
1
|
+
{"version":3,"file":"chart_block.d.ts","sourceRoot":"","sources":["../../src/ui/chart_block.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,mCAAmC;AACnC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,2BAA2B;AAC3B,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvE,wBAAwB;AACxB,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;CACzC;AAoBD,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD;AA6OD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,cAAc,GAAG,IAAI,CAoDlE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart_spec_from.d.ts","sourceRoot":"","sources":["../../src/ui/chart_spec_from.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"chart_spec_from.d.ts","sourceRoot":"","sources":["../../src/ui/chart_spec_from.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,kBAAkB,CAAC;AA6E7D,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAqC9D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart_tool.d.ts","sourceRoot":"","sources":["../../src/ui/chart_tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAInE,wDAAwD;AACxD,eAAO,MAAM,eAAe,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"chart_tool.d.ts","sourceRoot":"","sources":["../../src/ui/chart_tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAInE,wDAAwD;AACxD,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAgB9C,+BAA+B;AAC/B,wBAAgB,eAAe,IAAI,UAAU,CAuC5C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artooi/ag-ui-web-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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/core/ag_ui_chat.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { fillTemplate } from "../skills/fill_template.js";
|
|
25
25
|
import { parseSkills } from "../skills/parse_skills.js";
|
|
26
26
|
import type { Skill } from "../skills/skill.js";
|
|
27
|
+
import type { ChartRenderer } from "../tools/client_tool_registry.js";
|
|
27
28
|
import { type ClientTool, ClientToolRegistry } from "../tools/client_tool_registry.js";
|
|
28
29
|
import { isDestructive } from "../tools/is_destructive.js";
|
|
29
30
|
import { isNavigates } from "../tools/is_navigates.js";
|
|
@@ -42,7 +43,7 @@ import { renderAttachmentChips } from "../ui/attachment_chips.js";
|
|
|
42
43
|
import { AttachmentTray } from "../ui/attachment_tray.js";
|
|
43
44
|
import { renderChart } from "../ui/chart_block.js";
|
|
44
45
|
import { chartSpecFrom } from "../ui/chart_spec_from.js";
|
|
45
|
-
import { createChartTool } from "../ui/chart_tool.js";
|
|
46
|
+
import { CHART_TOOL_NAME, createChartTool } from "../ui/chart_tool.js";
|
|
46
47
|
import { CheckpointMenu, type CheckpointVerb } from "../ui/checkpoint_menu.js";
|
|
47
48
|
import { type ConfirmationRequest, requestConfirmation } from "../ui/confirmation_card.js";
|
|
48
49
|
import { prettifyToolName } from "../ui/prettify_tool_name.js";
|
|
@@ -85,7 +86,7 @@ import { RemoteConversationStore } from "./remote_conversation_store.js";
|
|
|
85
86
|
import { RunIndex } from "./run_index.js";
|
|
86
87
|
import { type TranscribeHandler, transcribeAudio } from "./transcribe_audio.js";
|
|
87
88
|
import { type UploadHandler, uploadAttachment } from "./upload_attachment.js";
|
|
88
|
-
import { withCredentials } from "./utils.js";
|
|
89
|
+
import { mintThread, withCredentials } from "./utils.js";
|
|
89
90
|
|
|
90
91
|
/** The role a rendered chat message takes. */
|
|
91
92
|
export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
|
|
@@ -1652,16 +1653,25 @@ export class AgUiChat extends HTMLElement {
|
|
|
1652
1653
|
}
|
|
1653
1654
|
|
|
1654
1655
|
/**
|
|
1655
|
-
* Start a fresh conversation:
|
|
1656
|
-
*
|
|
1656
|
+
* Start a fresh conversation: drop the in-memory run state, clear the
|
|
1657
|
+
* transcript, and mint a new thread id.
|
|
1658
|
+
*
|
|
1659
|
+
* The conversation being left is kept, and stays in the history drawer to
|
|
1660
|
+
* return to. Deleting one is the drawer row's own action; a button that
|
|
1661
|
+
* starts something new must not be the button that destroys what was there.
|
|
1657
1662
|
*/
|
|
1658
1663
|
newChat(): void {
|
|
1659
1664
|
// Stop any in-flight run first — discarding the client mid-run would
|
|
1660
1665
|
// leave the old agent streaming into a cleared transcript.
|
|
1661
1666
|
this.#cancelRun();
|
|
1662
|
-
|
|
1667
|
+
// A thread nothing was ever sent in has nothing to come back to, and the
|
|
1668
|
+
// drawer never listed it — so reap it here rather than strand one record
|
|
1669
|
+
// per press of a button whose whole use is being pressed again.
|
|
1670
|
+
if (this.conversationStore.isUnsent?.(this.#threadId) === true) {
|
|
1671
|
+
this.conversationStore.clear(this.#threadId);
|
|
1672
|
+
}
|
|
1663
1673
|
this.#resetState();
|
|
1664
|
-
this.#threadId = this.conversationStore
|
|
1674
|
+
this.#threadId = mintThread(this.conversationStore);
|
|
1665
1675
|
this.#setRunning(false);
|
|
1666
1676
|
this.#setUnread(0);
|
|
1667
1677
|
}
|
|
@@ -1840,9 +1850,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
1840
1850
|
this.#cardElements.set(restored.id, this.#cardFor(restored).element);
|
|
1841
1851
|
// Only `render` is replayed, never `handler`. A restored transcript
|
|
1842
1852
|
// redraws what the call drew; it must not re-run what the call *did*.
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1853
|
+
// Only the renderer is handed over, never the tool. The guarantee that
|
|
1854
|
+
// a reload cannot re-run a tool's *effect* is worth more than a comment
|
|
1855
|
+
// saying so: this signature cannot reach `handler`, so a later
|
|
1856
|
+
// maintainer adding a "no render? fall back to the handler" convenience
|
|
1857
|
+
// here has to change the type first, which is exactly the moment the
|
|
1858
|
+
// question should be asked.
|
|
1859
|
+
const render = this.#resolveTool(restored.name)?.render;
|
|
1860
|
+
if (render !== undefined) {
|
|
1861
|
+
this.#renderToolOutput(render, restored);
|
|
1846
1862
|
}
|
|
1847
1863
|
}
|
|
1848
1864
|
return;
|
|
@@ -2653,7 +2669,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
2653
2669
|
const result = await tool.handler(call.args, call.id);
|
|
2654
2670
|
// Drawn from the arguments rather than the result, so the live path and
|
|
2655
2671
|
// the replay path render the same thing from the same input.
|
|
2656
|
-
|
|
2672
|
+
if (tool.render !== undefined) {
|
|
2673
|
+
this.#renderToolOutput(tool.render, call);
|
|
2674
|
+
}
|
|
2657
2675
|
if (navigates) {
|
|
2658
2676
|
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.navigating);
|
|
2659
2677
|
return { content: "", halt: true };
|
|
@@ -3053,12 +3071,21 @@ export class AgUiChat extends HTMLElement {
|
|
|
3053
3071
|
* arrives is not something to switch on for everybody.
|
|
3054
3072
|
*/
|
|
3055
3073
|
enableCharts(routes: readonly ("tool" | "activity")[] = ["tool", "activity"]): void {
|
|
3074
|
+
const first = !this.#chartActivity && !this.#toolRegistry.has(CHART_TOOL_NAME);
|
|
3056
3075
|
if (routes.includes("activity")) {
|
|
3057
3076
|
this.#chartActivity = true;
|
|
3058
3077
|
}
|
|
3059
3078
|
if (routes.includes("tool")) {
|
|
3060
3079
|
this.registerTool(createChartTool());
|
|
3061
3080
|
}
|
|
3081
|
+
// Called after the element is connected, the history has already replayed
|
|
3082
|
+
// and every chart in it was skipped -- charts were off at the time. That is
|
|
3083
|
+
// the ordinary way to call this (you have to query the element to call
|
|
3084
|
+
// anything on it), so redrawing rather than documenting an ordering rule is
|
|
3085
|
+
// the only answer that does not make the obvious usage wrong.
|
|
3086
|
+
if (first && this.isConnected) {
|
|
3087
|
+
this.reload();
|
|
3088
|
+
}
|
|
3062
3089
|
}
|
|
3063
3090
|
|
|
3064
3091
|
/**
|
|
@@ -3071,13 +3098,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
3071
3098
|
* created inline, in the right place, so anchoring makes *when* the handler
|
|
3072
3099
|
* runs stop mattering.
|
|
3073
3100
|
*/
|
|
3074
|
-
#renderToolOutput(
|
|
3075
|
-
if (tool.render === undefined) {
|
|
3076
|
-
return;
|
|
3077
|
-
}
|
|
3101
|
+
#renderToolOutput(render: ChartRenderer, call: AgUiToolCall): void {
|
|
3078
3102
|
let node: Node | null;
|
|
3079
3103
|
try {
|
|
3080
|
-
node =
|
|
3104
|
+
node = render(call.args);
|
|
3081
3105
|
} catch (error) {
|
|
3082
3106
|
// `render` is consumer code and this runs inside the history replay, where
|
|
3083
3107
|
// a throw abandons the loop and takes every later turn of the transcript
|
|
@@ -3100,11 +3124,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
3100
3124
|
/** Draw, or redraw in place, the chart for one activity message. */
|
|
3101
3125
|
#drawActivityChart(messageId: string, content: unknown): void {
|
|
3102
3126
|
const spec = chartSpecFrom(content);
|
|
3103
|
-
|
|
3104
|
-
return;
|
|
3105
|
-
}
|
|
3106
|
-
const block = renderChart(spec);
|
|
3127
|
+
const block = spec === null ? null : renderChart(spec);
|
|
3107
3128
|
if (block === null) {
|
|
3129
|
+
// The server superseded this chart with something undrawable. Leaving the
|
|
3130
|
+
// old one up is the worst available answer: it shows numbers that have
|
|
3131
|
+
// been retracted, reading as current, and a reload then drops the chart
|
|
3132
|
+
// entirely because the *stored* content is the version we could not draw.
|
|
3133
|
+
// Live and reload should agree, and both should say "gone" rather than
|
|
3134
|
+
// one of them lying.
|
|
3135
|
+
this.#activityBlocks.get(messageId)?.remove();
|
|
3136
|
+
this.#activityBlocks.delete(messageId);
|
|
3108
3137
|
return;
|
|
3109
3138
|
}
|
|
3110
3139
|
const existing = this.#activityBlocks.get(messageId);
|
package/src/core/agui_client.ts
CHANGED
|
@@ -408,6 +408,9 @@ export class AgUiClient {
|
|
|
408
408
|
#buildSubscriber(pending: AgUiToolCall[], runState: RunState): AgentSubscriber {
|
|
409
409
|
const h = this.#handlers;
|
|
410
410
|
const closed = this.#closedMessageIds;
|
|
411
|
+
// Read at event time, not captured now: the flag flips mid-run, and the
|
|
412
|
+
// subscriber is built before the run that a later `cancel()` stops.
|
|
413
|
+
const cancelled = (): boolean => this.#cancelled;
|
|
411
414
|
// Charts whose patch has been dispatched but not yet applied. Scoped to the
|
|
412
415
|
// subscriber, so it cannot outlive the run that created it.
|
|
413
416
|
const pendingDeltas = new Set<string>();
|
|
@@ -510,6 +513,15 @@ export class AgUiClient {
|
|
|
510
513
|
onRunErrorEvent({ event }) {
|
|
511
514
|
runState.terminal = true;
|
|
512
515
|
runState.errored = true;
|
|
516
|
+
// Cancelling aborts the response mid-read, and the browser's own words
|
|
517
|
+
// for that can arrive here as a RUN_ERROR — Chrome's is
|
|
518
|
+
// "BodyStreamBuffer was aborted". The run is over either way, but a
|
|
519
|
+
// deliberate stop is not a failure, and reporting it would raise a
|
|
520
|
+
// warning bubble above the stopped note saying the same thing twice.
|
|
521
|
+
// The promise route in `#run` reports the cancellation.
|
|
522
|
+
if (cancelled()) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
513
525
|
h.onError(event.message);
|
|
514
526
|
},
|
|
515
527
|
onRunFinalized() {
|
|
@@ -532,7 +544,19 @@ interface RunState {
|
|
|
532
544
|
* Whether a rejection came from aborting the run's fetch. Belt-and-suspenders
|
|
533
545
|
* with the `#cancelled` flag: some `@ag-ui/client` versions re-throw the
|
|
534
546
|
* `AbortError` instead of filtering it.
|
|
547
|
+
*
|
|
548
|
+
* Aborting a fetch whose body is mid-read does not always surface as an
|
|
549
|
+
* `AbortError`. Chrome raises `TypeError: BodyStreamBuffer was aborted`, which
|
|
550
|
+
* is the same event wearing a different name, so a message naming the abort is
|
|
551
|
+
* read as one too. Narrow on purpose: only a `TypeError`, and only when it says
|
|
552
|
+
* so — a genuine type error carries no such word, and misreading one as a
|
|
553
|
+
* cancellation would hide a real failure behind a stopped note.
|
|
535
554
|
*/
|
|
536
555
|
function isAbortError(error: unknown): boolean {
|
|
537
|
-
|
|
556
|
+
if (!(error instanceof Error)) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
return (
|
|
560
|
+
error.name === "AbortError" || (error instanceof TypeError && /abort/i.test(error.message))
|
|
561
|
+
);
|
|
538
562
|
}
|
|
@@ -36,12 +36,24 @@ export interface ThreadMeta {
|
|
|
36
36
|
* is a small local hint a server store can derive from history and no-op.
|
|
37
37
|
*
|
|
38
38
|
* Thread enumeration backs the chat-history drawer; deleting a thread reuses
|
|
39
|
-
* {@link clear} and "new chat"
|
|
40
|
-
*
|
|
39
|
+
* {@link clear}, and "new chat" is {@link newThread} — which leaves the
|
|
40
|
+
* conversation it moves off of intact, for the drawer to offer back.
|
|
41
41
|
*/
|
|
42
42
|
export interface ClientConversationStore {
|
|
43
43
|
/** The active conversation id, generated and persisted on first read. */
|
|
44
44
|
threadId(): string;
|
|
45
|
+
/**
|
|
46
|
+
* Start a fresh conversation, make it active, and return its id.
|
|
47
|
+
*
|
|
48
|
+
* Existing threads are left where they are: "new chat" adds one, and
|
|
49
|
+
* {@link clear} is the only method that takes one away.
|
|
50
|
+
*
|
|
51
|
+
* Optional, so a store written before this method existed still works. The
|
|
52
|
+
* caller then mints the id itself and hands it to {@link setActiveThread},
|
|
53
|
+
* which loses only the store's own record that the thread is new (see
|
|
54
|
+
* {@link isUnsent}).
|
|
55
|
+
*/
|
|
56
|
+
newThread?(): string;
|
|
45
57
|
/** Load the persisted message history, or `null` when none exists. */
|
|
46
58
|
loadMessages(threadId: string): Promise<readonly Message[] | null>;
|
|
47
59
|
/** Persist the message history (and refresh the thread's drawer metadata). */
|
|
@@ -123,13 +135,12 @@ export class SessionStorageStore implements ClientConversationStore {
|
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
threadId(): string {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
138
|
+
return sessionStorage.getItem(this.#key(THREAD_SUFFIX)) ?? this.newThread();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
newThread(): string {
|
|
131
142
|
const id = randomUUID();
|
|
132
|
-
sessionStorage.setItem(key, id);
|
|
143
|
+
sessionStorage.setItem(this.#key(THREAD_SUFFIX), id);
|
|
133
144
|
sessionStorage.setItem(this.#key(MINTED_SUFFIX + id), "1");
|
|
134
145
|
return id;
|
|
135
146
|
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
SessionStorageStore,
|
|
6
6
|
type ThreadMeta,
|
|
7
7
|
} from "./conversation_store.js";
|
|
8
|
-
import { withCredentials } from "./utils.js";
|
|
8
|
+
import { mintThread, withCredentials } from "./utils.js";
|
|
9
9
|
|
|
10
10
|
/** One row of the server thread index (django-ag-ui's `ThreadsView` wire shape). */
|
|
11
11
|
interface ServerThreadRow {
|
|
@@ -69,6 +69,15 @@ export class RemoteConversationStore implements ClientConversationStore {
|
|
|
69
69
|
this.#local.setActiveThread(threadId);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Delegated to the local store, which owns the active id — and deliberately
|
|
74
|
+
* silent on the wire: the server learns of a thread when its first message is
|
|
75
|
+
* persisted, so an abandoned new chat costs no round-trip and leaves no row.
|
|
76
|
+
*/
|
|
77
|
+
newThread(): string {
|
|
78
|
+
return mintThread(this.#local);
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
/** Delegated, so wrapping a store does not lose what it knows about its own ids. */
|
|
73
82
|
isUnsent(threadId: string): boolean {
|
|
74
83
|
return this.#local.isUnsent?.(threadId) === true;
|
package/src/core/utils.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
// Non-exported-from-index helpers shared by the core
|
|
1
|
+
// Non-exported-from-index helpers shared by the core modules.
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from "@ag-ui/client";
|
|
4
|
+
import type { ClientConversationStore } from "./conversation_store.js";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Overlay a `credentials` mode onto a fetch `init`, or hand the `init` back
|
|
@@ -14,3 +17,24 @@ export function withCredentials(
|
|
|
14
17
|
): RequestInit | undefined {
|
|
15
18
|
return credentials === undefined ? init : { ...init, credentials };
|
|
16
19
|
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start a new conversation in `store` and return its id.
|
|
23
|
+
*
|
|
24
|
+
* `newThread` is optional on the interface, so a store that predates it is
|
|
25
|
+
* driven the only other way the interface allows: mint an id here and make it
|
|
26
|
+
* active. That path loses the store's own note that the thread is new, so a
|
|
27
|
+
* remote store would go on to ask the server for a conversation that cannot
|
|
28
|
+
* exist yet — which is why every store in this package implements the method.
|
|
29
|
+
*
|
|
30
|
+
* What neither path does is clear the thread being left behind. Starting a
|
|
31
|
+
* conversation is not a reason to destroy the previous one.
|
|
32
|
+
*/
|
|
33
|
+
export function mintThread(store: ClientConversationStore): string {
|
|
34
|
+
if (store.newThread !== undefined) {
|
|
35
|
+
return store.newThread();
|
|
36
|
+
}
|
|
37
|
+
const id = randomUUID();
|
|
38
|
+
store.setActiveThread(id);
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
@@ -7,6 +7,16 @@ import type { Tool } from "@ag-ui/core";
|
|
|
7
7
|
* `handler` receives the parsed arguments and returns a result that is
|
|
8
8
|
* JSON-serialised into the AG-UI tool-result message sent back to the agent.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Draws one call from its arguments alone.
|
|
12
|
+
*
|
|
13
|
+
* Named separately so the replay path can take *this* and never the tool that
|
|
14
|
+
* owns it: a function of this type cannot reach a `handler`, which is what
|
|
15
|
+
* makes "a reload never re-runs a tool's effect" a property of the code rather
|
|
16
|
+
* than a note asking maintainers to be careful.
|
|
17
|
+
*/
|
|
18
|
+
export type ChartRenderer = (args: Record<string, unknown>) => Node | null;
|
|
19
|
+
|
|
10
20
|
export interface ClientTool {
|
|
11
21
|
name: string;
|
|
12
22
|
description: string;
|
|
@@ -34,7 +44,7 @@ export interface ClientTool {
|
|
|
34
44
|
*
|
|
35
45
|
* Return `null` for arguments that say nothing worth drawing.
|
|
36
46
|
*/
|
|
37
|
-
render?:
|
|
47
|
+
render?: ChartRenderer;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
/**
|
package/src/ui/chart_block.ts
CHANGED
|
@@ -174,7 +174,12 @@ function drawStacked(svg: SVGSVGElement, spec: ChartSpec, min: number, max: numb
|
|
|
174
174
|
const running = spec.labels.map(() => 0);
|
|
175
175
|
spec.series.forEach((series, s) => {
|
|
176
176
|
series.points.forEach((value, i) => {
|
|
177
|
-
|
|
177
|
+
// `?? 0` rather than a cast: `renderChart` is exported, so a caller can
|
|
178
|
+
// hand it a series carrying more points than there are labels, which
|
|
179
|
+
// `chartSpecFrom` would have refused. The cast that used to be here
|
|
180
|
+
// claimed that could not happen and wrote `y="NaN"` into the DOM when it
|
|
181
|
+
// did.
|
|
182
|
+
const from = running[i] ?? 0;
|
|
178
183
|
const to = from + value;
|
|
179
184
|
running[i] = to;
|
|
180
185
|
const y = scaleY(to, min, max);
|
|
@@ -22,6 +22,17 @@ const KINDS: readonly ChartKind[] = ["bar", "line", "pie", "scatter", "stacked"]
|
|
|
22
22
|
*/
|
|
23
23
|
const MAX_POINTS = 20_000;
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Whether a spec's *labels* are cheap enough to draw.
|
|
27
|
+
*
|
|
28
|
+
* `MAX_POINTS` bounds the data; this bounds the DOM. Every label produces an
|
|
29
|
+
* axis text node whatever the series count, so a spec well inside the point
|
|
30
|
+
* budget can still emit tens of thousands of nodes and block the main thread —
|
|
31
|
+
* again on every reload, since it is in the transcript. Kept separate because
|
|
32
|
+
* the two limits answer different questions and a single number cannot.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_LABELS = 2_000;
|
|
35
|
+
|
|
25
36
|
/**
|
|
26
37
|
* Largest magnitude a point may carry.
|
|
27
38
|
*
|
|
@@ -59,9 +70,17 @@ function asNumbers(value: unknown): number[] | null {
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
function asStrings(value: unknown): string[] | null {
|
|
62
|
-
if (!Array.isArray(value)
|
|
73
|
+
if (!Array.isArray(value)) {
|
|
63
74
|
return null;
|
|
64
75
|
}
|
|
76
|
+
// `every` over an index range rather than `Array.prototype.some`, which skips
|
|
77
|
+
// holes: a sparse array passed the old check and drew a chart with blank axis
|
|
78
|
+
// labels, which reads as a rendering bug rather than bad input.
|
|
79
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
80
|
+
if (typeof value[i] !== "string") {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
65
84
|
return value as string[];
|
|
66
85
|
}
|
|
67
86
|
|
|
@@ -94,7 +113,7 @@ export function chartSpecFrom(value: unknown): ChartSpec | null {
|
|
|
94
113
|
if (series.length === 0) {
|
|
95
114
|
return null;
|
|
96
115
|
}
|
|
97
|
-
if (series.length * labels.length > MAX_POINTS) {
|
|
116
|
+
if (series.length * labels.length > MAX_POINTS || labels.length > MAX_LABELS) {
|
|
98
117
|
return null;
|
|
99
118
|
}
|
|
100
119
|
|
package/src/ui/chart_tool.ts
CHANGED
|
@@ -20,6 +20,12 @@ function draw(args: Record<string, unknown>): HTMLDivElement | null {
|
|
|
20
20
|
return spec === null ? null : renderChart(spec);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Whether these arguments would produce a chart, without producing one. */
|
|
24
|
+
function drawable(args: Record<string, unknown>): boolean {
|
|
25
|
+
const spec = chartSpecFrom(args);
|
|
26
|
+
return spec !== null && spec.labels.length > 0 && spec.series.length > 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
const REJECTED =
|
|
24
30
|
"chart not rendered: expected labels (strings) and series, each with one finite number per label";
|
|
25
31
|
|
|
@@ -54,11 +60,13 @@ export function createChartTool(): ClientTool {
|
|
|
54
60
|
// Says what happened and nothing else; the drawing is `render`'s job. Told
|
|
55
61
|
// plainly when the arguments are unusable, because the model can fix that
|
|
56
62
|
// and retry — a silent no-op would leave it believing the chart is on screen.
|
|
57
|
-
// Answers on what
|
|
58
|
-
// pass validation and still have nothing to show
|
|
59
|
-
//
|
|
60
|
-
// chart is
|
|
61
|
-
|
|
63
|
+
// Answers on what will actually be drawn, not on what validated: a spec can
|
|
64
|
+
// pass validation and still have nothing to show, and reporting success
|
|
65
|
+
// then would leave the model believing a chart is on screen. Asks the
|
|
66
|
+
// question without building the chart, because `render` is about to build
|
|
67
|
+
// the same one a moment later and drawing it twice is pure waste on a spec
|
|
68
|
+
// large enough to matter.
|
|
69
|
+
handler: (args: Record<string, unknown>) => (drawable(args) ? "chart rendered" : REJECTED),
|
|
62
70
|
render: draw,
|
|
63
71
|
};
|
|
64
72
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.27.0";
|