@artooi/ag-ui-web-component 0.11.0 → 0.13.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +86 -1
  2. package/README.md +98 -5
  3. package/dist/ag-ui-web-component.bundle.js +126 -50
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +9 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +29 -4
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +11 -0
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/core/create_http_agent.d.ts +6 -0
  12. package/dist/core/create_http_agent.d.ts.map +1 -1
  13. package/dist/core/run_index.d.ts +50 -0
  14. package/dist/core/run_index.d.ts.map +1 -0
  15. package/dist/index.d.ts +9 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +453 -63
  18. package/dist/index.js.map +3 -3
  19. package/dist/tools/page_state.d.ts +40 -0
  20. package/dist/tools/page_state.d.ts.map +1 -0
  21. package/dist/ui/checkpoint_menu.d.ts +32 -0
  22. package/dist/ui/checkpoint_menu.d.ts.map +1 -0
  23. package/dist/ui/styles.d.ts +1 -1
  24. package/dist/ui/styles.d.ts.map +1 -1
  25. package/dist/ui/ui_strings.d.ts +10 -0
  26. package/dist/ui/ui_strings.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/constants.ts +10 -0
  29. package/src/core/ag_ui_chat.ts +153 -6
  30. package/src/core/agui_client.ts +28 -0
  31. package/src/core/create_http_agent.ts +7 -0
  32. package/src/core/run_index.ts +91 -0
  33. package/src/index.ts +14 -1
  34. package/src/tools/page_state.ts +72 -0
  35. package/src/ui/checkpoint_menu.ts +153 -0
  36. package/src/ui/styles.ts +76 -0
  37. package/src/ui/ui_strings.ts +15 -0
  38. package/src/version.ts +1 -1
  39. package/dist/tools/state_hook.d.ts +0 -23
  40. package/dist/tools/state_hook.d.ts.map +0 -1
  41. package/src/tools/state_hook.ts +0 -53
@@ -18,6 +18,12 @@ export interface HttpAgentOptions {
18
18
  threadId?: string;
19
19
  /** Rehydrated history to seed the agent with (durable conversation). */
20
20
  initialMessages?: readonly Message[];
21
+ /**
22
+ * AG-UI shared state to seed the agent with. `@ag-ui/client` sends it as
23
+ * `RunAgentInput.state` on every run and replaces it in place when the
24
+ * server streams `STATE_SNAPSHOT` / `STATE_DELTA`.
25
+ */
26
+ initialState?: Readonly<Record<string, unknown>>;
21
27
  }
22
28
 
23
29
  /**
@@ -31,6 +37,7 @@ export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
31
37
  return new HttpAgent({
32
38
  url: options.endpoint,
33
39
  headers: options.headers ?? {},
40
+ initialState: { ...(options.initialState ?? {}) },
34
41
  // HttpAgent invokes its configured fetch as a method (`this.fetch(...)`),
35
42
  // which would rebind the global `fetch` to the agent instance and trigger
36
43
  // "Illegal invocation" in browsers. Wrap it so `fetch` is always called as
@@ -0,0 +1,91 @@
1
+ /** One row of the server run index (django-ag-ui's `RunsView` wire shape). */
2
+ export interface RunRow {
3
+ readonly run_id: string;
4
+ readonly thread_id: string | null;
5
+ readonly parent_run_id: string | null;
6
+ readonly started_at: string | null;
7
+ /** Whether the run has a snapshot to seed from — see {@link RunIndex}. */
8
+ readonly continuable: boolean;
9
+ }
10
+
11
+ /** Live header source, read per request so rotated tokens / CSRF reach the server. */
12
+ type HeadersProvider = () => Record<string, string>;
13
+
14
+ /**
15
+ * Reads the server's run index and derives the resume / fork URLs beside it.
16
+ *
17
+ * Backed by django-ag-ui's owner-scoped `RunsView` — the URL passed to
18
+ * `<ag-ui-chat>` as `data-runs-url`:
19
+ *
20
+ * - `GET <url>` → the user's runs, newest first.
21
+ *
22
+ * **Only `continuable` rows can be resumed.** The server reports whether a run
23
+ * has a saved snapshot to seed from; a run that never reached a provider-valid
24
+ * boundary has none, so resuming it would start from nothing. Callers should
25
+ * offer the action only for those rows and treat the rest as informational — a
26
+ * crashed run worth showing, not worth continuing. {@link continuable} filters
27
+ * for exactly that.
28
+ *
29
+ * **Resume and fork are siblings of the index**, not separate configuration.
30
+ * django-ag-ui mounts all three under one prefix whenever a step store is
31
+ * configured (`runs/`, `resume/<id>/`, `fork/<id>/`), so one URL locates them
32
+ * all and there is no way to configure a half-working set.
33
+ */
34
+ export class RunIndex {
35
+ readonly #url: string;
36
+ readonly #headers: HeadersProvider;
37
+
38
+ constructor(url: string, headers: HeadersProvider = () => ({})) {
39
+ this.#url = url.endsWith("/") ? url : `${url}/`;
40
+ this.#headers = headers;
41
+ }
42
+
43
+ /**
44
+ * The user's runs, or `[]` when the endpoint is unreachable or answers with
45
+ * an error. A history affordance that cannot load is empty, never broken:
46
+ * the caller renders its empty state rather than surfacing a transport fault
47
+ * the user can do nothing about.
48
+ */
49
+ async list(): Promise<readonly RunRow[]> {
50
+ try {
51
+ const response = await fetch(this.#url, {
52
+ method: "GET",
53
+ headers: { Accept: "application/json", ...this.#headers() },
54
+ });
55
+ if (!response.ok) {
56
+ return [];
57
+ }
58
+ const body = (await response.json()) as { runs?: readonly RunRow[] };
59
+ return body.runs ?? [];
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+
65
+ /** The rows a client may actually continue. */
66
+ async continuable(): Promise<readonly RunRow[]> {
67
+ return (await this.list()).filter((run) => run.continuable);
68
+ }
69
+
70
+ /** The endpoint that continues `runId` as a new run. */
71
+ resumeUrl(runId: string): string {
72
+ return this.#sibling("resume", runId);
73
+ }
74
+
75
+ /** The endpoint that branches `runId` into a new run, leaving the source untouched. */
76
+ forkUrl(runId: string): string {
77
+ return this.#sibling("fork", runId);
78
+ }
79
+
80
+ /**
81
+ * `<mount>/<verb>/<runId>/`, derived from the index URL's own prefix.
82
+ *
83
+ * Built by string surgery on the trailing `runs/` rather than with `new URL`,
84
+ * because the configured value may be root-relative (`/agent/runs/`) — the
85
+ * common case in a Django template — and `new URL` needs an absolute base.
86
+ */
87
+ #sibling(verb: string, runId: string): string {
88
+ const prefix = this.#url.slice(0, -"runs/".length);
89
+ return `${prefix}${verb}/${encodeURIComponent(runId)}/`;
90
+ }
91
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export {
4
4
  ELEMENT_TAG,
5
5
  MAX_TOOL_ROUNDS,
6
6
  MESSAGE_ROLE,
7
+ STATE_EVENT,
7
8
  SUBMIT_EVENT,
8
9
  TOGGLE_EVENT,
9
10
  TOOL_CALL_STATUS,
@@ -16,6 +17,7 @@ export {
16
17
  export {
17
18
  AgUiChat,
18
19
  type MessageRole,
20
+ type StateDetail,
19
21
  type SubmitDetail,
20
22
  type ToggleDetail,
21
23
  } from "./core/ag_ui_chat.js";
@@ -45,6 +47,7 @@ export {
45
47
  } from "./core/create_http_agent.js";
46
48
  export { defineAgUiChat } from "./core/define_ag_ui_chat.js";
47
49
  export { RemoteConversationStore } from "./core/remote_conversation_store.js";
50
+ export { RunIndex, type RunRow } from "./core/run_index.js";
48
51
  export {
49
52
  type TranscribeHandler,
50
53
  type TranscribeOptions,
@@ -92,6 +95,16 @@ export {
92
95
  type ResolvePageTarget,
93
96
  } from "./tools/page_action_tools.js";
94
97
  export { createPageMapContext, type PageMap } from "./tools/page_map.js";
98
+ /**
99
+ * @deprecated Renamed to `createPageStateTools` / `PageState`. The old names
100
+ * read as AG-UI shared-state sync, which this component does not implement.
101
+ */
102
+ export {
103
+ createPageStateTools,
104
+ createStateHookTools,
105
+ type PageState,
106
+ type StateHook,
107
+ } from "./tools/page_state.js";
95
108
  export { parseToolCatalog, type ToolCatalogEntry } from "./tools/parse_tool_catalog.js";
96
109
  export {
97
110
  createRouteTools,
@@ -99,13 +112,13 @@ export {
99
112
  type RouteMap,
100
113
  type RouteWithParams,
101
114
  } from "./tools/route_map.js";
102
- export { createStateHookTools, type StateHook } from "./tools/state_hook.js";
103
115
  export {
104
116
  type ApprovalOptions,
105
117
  type ApprovalRenderer,
106
118
  type ApprovalRequest,
107
119
  requestApproval,
108
120
  } from "./ui/approval_card.js";
121
+ export { CheckpointMenu, type CheckpointVerb } from "./ui/checkpoint_menu.js";
109
122
  export {
110
123
  type ConfirmationOptions,
111
124
  type ConfirmationRequest,
@@ -0,0 +1,72 @@
1
+ import { X_DESTRUCTIVE_KEY, X_SUMMARY_KEY } from "../constants.js";
2
+ import type { ClientTool } from "./client_tool_registry.js";
3
+
4
+ /**
5
+ * A binding from a named piece of host page state to agent tools.
6
+ *
7
+ * Ergonomic sugar over `registerTool` for SPA state (Redux/Zustand/signals):
8
+ * generates a read tool and, when `write` is given, a destructive set tool.
9
+ *
10
+ * This is **not** AG-UI shared state. `STATE_SNAPSHOT` / `STATE_DELTA` are
11
+ * protocol events carrying state between agent and client; this is a pair of
12
+ * ordinary client tools the agent calls like any other. The two are unrelated,
13
+ * which is why this no longer carries the word "hook" — the old name read as
14
+ * protocol state sync, which this component does not implement.
15
+ */
16
+ export interface PageState {
17
+ /** Base name; tools become `read_<name>` and `set_<name>`. */
18
+ readonly name: string;
19
+ /** Returns the current state value. */
20
+ readonly read: () => unknown;
21
+ /** Mutates the state from the agent-supplied args. Omit for read-only. */
22
+ readonly write?: (args: Record<string, unknown>) => unknown;
23
+ /** JSON-Schema for the set tool's args. Defaults to an open object. */
24
+ readonly schema?: Record<string, unknown>;
25
+ }
26
+
27
+ /**
28
+ * Build the tools for a {@link PageState}: always a `read_<name>` (read-only)
29
+ * tool, plus a `set_<name>` (`x-destructive`) tool when `write` is supplied.
30
+ */
31
+ export function createPageStateTools(binding: PageState): ClientTool[] {
32
+ const tools: ClientTool[] = [
33
+ {
34
+ name: `read_${binding.name}`,
35
+ description: `Read the "${binding.name}" state.`,
36
+ parameters: {
37
+ type: "object",
38
+ properties: {},
39
+ required: [],
40
+ [X_SUMMARY_KEY]: `Read ${binding.name}`,
41
+ },
42
+ handler: () => binding.read(),
43
+ },
44
+ ];
45
+ const write = binding.write;
46
+ if (write !== undefined) {
47
+ tools.push({
48
+ name: `set_${binding.name}`,
49
+ description: `Update the "${binding.name}" state.`,
50
+ parameters: {
51
+ ...(binding.schema ?? { type: "object" }),
52
+ [X_DESTRUCTIVE_KEY]: true,
53
+ [X_SUMMARY_KEY]: `Update ${binding.name}`,
54
+ },
55
+ handler: (args) => write(args),
56
+ });
57
+ }
58
+ return tools;
59
+ }
60
+
61
+ /**
62
+ * @deprecated Renamed to {@link PageState}. The old name read as AG-UI
63
+ * shared-state sync (`STATE_SNAPSHOT` / `STATE_DELTA`), which this component
64
+ * does not implement. Will be removed in a future major.
65
+ */
66
+ export type StateHook = PageState;
67
+
68
+ /**
69
+ * @deprecated Renamed to {@link createPageStateTools}. Will be removed in a
70
+ * future major.
71
+ */
72
+ export const createStateHookTools = createPageStateTools;
@@ -0,0 +1,153 @@
1
+ import type { RunRow } from "../core/run_index.js";
2
+ import { relativeTime } from "./relative_time.js";
3
+ import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
4
+
5
+ /** How the host continues a picked run. */
6
+ export type CheckpointVerb = "resume" | "fork";
7
+
8
+ /**
9
+ * The checkpoint panel: continuable runs, each offering **resume** or **fork**.
10
+ *
11
+ * A separate surface from the thread drawer on purpose — they are different
12
+ * axes. A thread is a conversation you switch *to*; a checkpoint is a run you
13
+ * continue *from*, and one thread can hold many. Folding them into one list
14
+ * would make "resume" look like "open", which it isn't: resuming starts a new
15
+ * run seeded from a snapshot.
16
+ *
17
+ * Only rows the server marked `continuable` are worth offering, so the host
18
+ * feeds those; a run with no snapshot would resume from nothing. Pure DOM in
19
+ * the spirit of {@link SkillsMenu} — the host appends {@link element}, toggles
20
+ * it, feeds rows via {@link setRuns}, and acts on {@link onPick}.
21
+ */
22
+ export class CheckpointMenu {
23
+ /** The panel root. Append to the chat shell; hidden until opened. */
24
+ readonly element: HTMLDivElement;
25
+
26
+ readonly #onPick: (runId: string, verb: CheckpointVerb) => void;
27
+ readonly #list: HTMLDivElement;
28
+ readonly #heading: HTMLSpanElement;
29
+ #strings: UiStrings;
30
+ #runs: readonly RunRow[] = [];
31
+
32
+ constructor(
33
+ onPick: (runId: string, verb: CheckpointVerb) => void,
34
+ strings: UiStrings = DEFAULT_UI_STRINGS,
35
+ ) {
36
+ this.#onPick = onPick;
37
+ this.#strings = strings;
38
+
39
+ this.element = document.createElement("div");
40
+ this.element.className = "checkpoints";
41
+ this.element.setAttribute("part", "checkpoints");
42
+ this.element.setAttribute("role", "dialog");
43
+ this.element.setAttribute("aria-label", strings.checkpoints);
44
+ this.element.hidden = true;
45
+
46
+ const header = document.createElement("div");
47
+ header.className = "checkpoints-header";
48
+ header.setAttribute("part", "checkpoints-header");
49
+ this.#heading = document.createElement("span");
50
+ this.#heading.className = "checkpoints-title";
51
+ this.#heading.textContent = strings.checkpoints;
52
+ header.append(this.#heading);
53
+
54
+ this.#list = document.createElement("div");
55
+ this.#list.className = "checkpoints-list";
56
+ this.#list.setAttribute("part", "checkpoints-list");
57
+
58
+ this.element.append(header, this.#list);
59
+ this.element.addEventListener("keydown", (event) => {
60
+ if (event.key === "Escape") {
61
+ event.stopPropagation();
62
+ this.close();
63
+ }
64
+ });
65
+ }
66
+
67
+ /** Replace the rows. The host passes only `continuable` runs. */
68
+ setRuns(runs: readonly RunRow[]): void {
69
+ this.#runs = runs;
70
+ this.#render();
71
+ }
72
+
73
+ /** Re-localize a panel built before the host's strings resolved. */
74
+ setStrings(strings: UiStrings): void {
75
+ this.#strings = strings;
76
+ this.element.setAttribute("aria-label", strings.checkpoints);
77
+ this.#heading.textContent = strings.checkpoints;
78
+ this.#render();
79
+ }
80
+
81
+ open(): void {
82
+ this.element.hidden = false;
83
+ }
84
+
85
+ close(): void {
86
+ this.element.hidden = true;
87
+ }
88
+
89
+ get open_(): boolean {
90
+ return !this.element.hidden;
91
+ }
92
+
93
+ #render(): void {
94
+ this.#list.replaceChildren();
95
+ if (this.#runs.length === 0) {
96
+ const empty = document.createElement("div");
97
+ empty.className = "checkpoints-empty";
98
+ empty.setAttribute("part", "checkpoints-empty");
99
+ empty.textContent = this.#strings.noCheckpoints;
100
+ this.#list.append(empty);
101
+ return;
102
+ }
103
+ for (const run of this.#runs) {
104
+ this.#list.append(this.#row(run));
105
+ }
106
+ }
107
+
108
+ #row(run: RunRow): HTMLDivElement {
109
+ const row = document.createElement("div");
110
+ row.className = "checkpoint-row";
111
+ row.setAttribute("part", "checkpoint-row");
112
+
113
+ const label = document.createElement("span");
114
+ label.className = "checkpoint-label";
115
+ // A run id is opaque to a person, so the time is the identifying detail;
116
+ // the id rides `title` for anyone who needs to correlate with server logs.
117
+ label.textContent =
118
+ run.started_at === null
119
+ ? run.run_id
120
+ : relativeTime(Date.parse(run.started_at), Date.now(), this.#strings);
121
+ label.title = run.run_id;
122
+ row.append(label);
123
+
124
+ if (run.parent_run_id !== null) {
125
+ // Lineage, so a branch doesn't read as a duplicate of its parent.
126
+ const branch = document.createElement("span");
127
+ branch.className = "checkpoint-branch";
128
+ branch.setAttribute("part", "checkpoint-branch");
129
+ branch.textContent = this.#strings.forkedRun;
130
+ branch.title = run.parent_run_id;
131
+ row.append(branch);
132
+ }
133
+
134
+ row.append(
135
+ this.#action(run.run_id, "resume", this.#strings.resumeRun),
136
+ this.#action(run.run_id, "fork", this.#strings.forkRun),
137
+ );
138
+ return row;
139
+ }
140
+
141
+ #action(runId: string, verb: CheckpointVerb, label: string): HTMLButtonElement {
142
+ const button = document.createElement("button");
143
+ button.type = "button";
144
+ button.className = `checkpoint-action checkpoint-${verb}`;
145
+ button.setAttribute("part", `checkpoint-action checkpoint-${verb}`);
146
+ button.textContent = label;
147
+ button.addEventListener("click", () => {
148
+ this.close();
149
+ this.#onPick(runId, verb);
150
+ });
151
+ return button;
152
+ }
153
+ }
package/src/ui/styles.ts CHANGED
@@ -1229,6 +1229,82 @@ export const STYLES = `
1229
1229
  display: none;
1230
1230
  }
1231
1231
 
1232
+ .checkpoints {
1233
+ position: absolute;
1234
+ inset-block-start: 3rem;
1235
+ inset-inline: 0.75rem;
1236
+ z-index: 6;
1237
+ display: flex;
1238
+ flex-direction: column;
1239
+ gap: 0.25rem;
1240
+ padding: 0.5rem;
1241
+ border: 1px solid var(--agui-border, #d7d7dc);
1242
+ border-radius: 0.5rem;
1243
+ background: var(--agui-surface, #fff);
1244
+ box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
1245
+ max-height: 60%;
1246
+ overflow-y: auto;
1247
+ }
1248
+
1249
+ .checkpoints[hidden] {
1250
+ display: none;
1251
+ }
1252
+
1253
+ .checkpoints-title {
1254
+ font-size: 0.75rem;
1255
+ font-weight: 600;
1256
+ opacity: 0.7;
1257
+ }
1258
+
1259
+ .checkpoints-empty {
1260
+ padding: 0.5rem 0.25rem;
1261
+ font-size: 0.8125rem;
1262
+ opacity: 0.7;
1263
+ }
1264
+
1265
+ .checkpoint-row {
1266
+ display: flex;
1267
+ align-items: center;
1268
+ gap: 0.5rem;
1269
+ padding: 0.25rem;
1270
+ border-radius: 0.375rem;
1271
+ }
1272
+
1273
+ .checkpoint-row:hover {
1274
+ background: var(--agui-hover, #f3f3f5);
1275
+ }
1276
+
1277
+ .checkpoint-label {
1278
+ flex: 1;
1279
+ font-size: 0.8125rem;
1280
+ white-space: nowrap;
1281
+ overflow: hidden;
1282
+ text-overflow: ellipsis;
1283
+ }
1284
+
1285
+ .checkpoint-branch {
1286
+ font-size: 0.6875rem;
1287
+ padding: 0 0.375rem;
1288
+ border-radius: 999px;
1289
+ background: var(--agui-hover, #f3f3f5);
1290
+ opacity: 0.8;
1291
+ }
1292
+
1293
+ .checkpoint-action {
1294
+ font: inherit;
1295
+ font-size: 0.75rem;
1296
+ cursor: pointer;
1297
+ padding: 0.125rem 0.5rem;
1298
+ border: 1px solid var(--agui-border, #d7d7dc);
1299
+ border-radius: 0.375rem;
1300
+ background: transparent;
1301
+ color: inherit;
1302
+ }
1303
+
1304
+ .checkpoint-action:hover {
1305
+ background: var(--agui-hover, #f3f3f5);
1306
+ }
1307
+
1232
1308
  .drawer-backdrop {
1233
1309
  position: absolute;
1234
1310
  inset: 0;
@@ -157,6 +157,16 @@ export interface UiStrings {
157
157
  minutesAgo: string;
158
158
  /** Hours ago. Token: `{n}`. */
159
159
  hoursAgo: string;
160
+ /** Title of the checkpoint panel. */
161
+ checkpoints: string;
162
+ /** Empty state when no run can be continued. */
163
+ noCheckpoints: string;
164
+ /** Action: continue a run from its last checkpoint. */
165
+ resumeRun: string;
166
+ /** Action: branch a run without touching the original. */
167
+ forkRun: string;
168
+ /** Badge on a run that branched from another. */
169
+ forkedRun: string;
160
170
  /** Days ago. Token: `{n}`. */
161
171
  daysAgo: string;
162
172
  /** Weeks ago. Token: `{n}`. */
@@ -171,6 +181,11 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
171
181
  collapse: "Collapse",
172
182
  expand: "Expand",
173
183
  toggleTheme: "Toggle theme",
184
+ checkpoints: "Continue a run",
185
+ noCheckpoints: "Nothing to continue yet.",
186
+ resumeRun: "Resume",
187
+ forkRun: "Fork",
188
+ forkedRun: "branched",
174
189
 
175
190
  conversation: "Conversation",
176
191
  thinking: "Assistant is thinking…",
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.11.0";
1
+ export const VERSION: string = "0.13.0";
@@ -1,23 +0,0 @@
1
- import type { ClientTool } from "./client_tool_registry.js";
2
- /**
3
- * A binding from a named piece of host application state to agent tools.
4
- *
5
- * Ergonomic sugar over `registerTool` for SPA state (Redux/Zustand/signals):
6
- * generates a read tool and, when `write` is given, a destructive set tool.
7
- */
8
- export interface StateHook {
9
- /** Base name; tools become `read_<name>` and `set_<name>`. */
10
- readonly name: string;
11
- /** Returns the current state value. */
12
- readonly read: () => unknown;
13
- /** Mutates the state from the agent-supplied args. Omit for read-only. */
14
- readonly write?: (args: Record<string, unknown>) => unknown;
15
- /** JSON-Schema for the set tool's args. Defaults to an open object. */
16
- readonly schema?: Record<string, unknown>;
17
- }
18
- /**
19
- * Build the tools for a {@link StateHook}: always a `read_<name>` (read-only)
20
- * tool, plus a `set_<name>` (`x-destructive`) tool when `write` is supplied.
21
- */
22
- export declare function createStateHookTools(hook: StateHook): ClientTool[];
23
- //# sourceMappingURL=state_hook.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"state_hook.d.ts","sourceRoot":"","sources":["../../src/tools/state_hook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAC5D,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,EAAE,CA4BlE"}
@@ -1,53 +0,0 @@
1
- import { X_DESTRUCTIVE_KEY, X_SUMMARY_KEY } from "../constants.js";
2
- import type { ClientTool } from "./client_tool_registry.js";
3
-
4
- /**
5
- * A binding from a named piece of host application state to agent tools.
6
- *
7
- * Ergonomic sugar over `registerTool` for SPA state (Redux/Zustand/signals):
8
- * generates a read tool and, when `write` is given, a destructive set tool.
9
- */
10
- export interface StateHook {
11
- /** Base name; tools become `read_<name>` and `set_<name>`. */
12
- readonly name: string;
13
- /** Returns the current state value. */
14
- readonly read: () => unknown;
15
- /** Mutates the state from the agent-supplied args. Omit for read-only. */
16
- readonly write?: (args: Record<string, unknown>) => unknown;
17
- /** JSON-Schema for the set tool's args. Defaults to an open object. */
18
- readonly schema?: Record<string, unknown>;
19
- }
20
-
21
- /**
22
- * Build the tools for a {@link StateHook}: always a `read_<name>` (read-only)
23
- * tool, plus a `set_<name>` (`x-destructive`) tool when `write` is supplied.
24
- */
25
- export function createStateHookTools(hook: StateHook): ClientTool[] {
26
- const tools: ClientTool[] = [
27
- {
28
- name: `read_${hook.name}`,
29
- description: `Read the "${hook.name}" state.`,
30
- parameters: {
31
- type: "object",
32
- properties: {},
33
- required: [],
34
- [X_SUMMARY_KEY]: `Read ${hook.name}`,
35
- },
36
- handler: () => hook.read(),
37
- },
38
- ];
39
- const write = hook.write;
40
- if (write !== undefined) {
41
- tools.push({
42
- name: `set_${hook.name}`,
43
- description: `Update the "${hook.name}" state.`,
44
- parameters: {
45
- ...(hook.schema ?? { type: "object" }),
46
- [X_DESTRUCTIVE_KEY]: true,
47
- [X_SUMMARY_KEY]: `Update ${hook.name}`,
48
- },
49
- handler: (args) => write(args),
50
- });
51
- }
52
- return tools;
53
- }