akanjs 3.0.0-alpha.52 → 3.0.0-alpha.53

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 (67) hide show
  1. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  2. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  3. package/package.json +1 -1
  4. package/server/routeTreeBuilder.ts +10 -8
  5. package/service/predefinedAdaptor/llm.adaptor.ts +0 -1
  6. package/store/agent/StoreSurfaceSource.ts +1 -5
  7. package/store/agentic/AgentValue.ts +112 -0
  8. package/store/agentic/StExposeBuilder.ts +47 -0
  9. package/store/agentic/StExposeDraft.ts +24 -0
  10. package/store/agentic/StStateBuilder.ts +85 -0
  11. package/store/agentic/StStateDraft.ts +23 -0
  12. package/store/agentic/StToolBuilder.ts +43 -25
  13. package/store/agentic/StToolDraft.ts +22 -0
  14. package/store/agentic/attachAgentic.ts +20 -14
  15. package/store/agentic/index.ts +6 -3
  16. package/store/agentic/useFieldTool.ts +0 -4
  17. package/store/agentic/useFormTools.ts +4 -6
  18. package/store/agentic/useRelationFieldTool.ts +1 -2
  19. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +0 -1
  20. package/types/store/agentic/AgentValue.d.ts +35 -0
  21. package/types/store/agentic/StExposeBuilder.d.ts +21 -0
  22. package/types/store/agentic/StExposeDraft.d.ts +13 -0
  23. package/types/store/agentic/StStateBuilder.d.ts +21 -0
  24. package/types/store/agentic/StStateDraft.d.ts +12 -0
  25. package/types/store/agentic/StToolBuilder.d.ts +15 -16
  26. package/types/store/agentic/StToolDraft.d.ts +13 -0
  27. package/types/store/agentic/attachAgentic.d.ts +17 -11
  28. package/types/store/agentic/index.d.ts +6 -3
  29. package/types/store/agentic/useFormTools.d.ts +4 -4
  30. package/types/vendor/use-agentic/Agentic.d.ts +3 -3
  31. package/types/vendor/use-agentic/AgenticSurface.d.ts +5 -4
  32. package/types/vendor/use-agentic/types.d.ts +5 -5
  33. package/types/vendor/use-agentic/useAgentTool.d.ts +2 -2
  34. package/ui/Agent/Tool.tsx +1 -15
  35. package/ui/Data/ListContainer.tsx +20 -42
  36. package/ui/Dialog/Provider.tsx +7 -6
  37. package/ui/Dropdown.tsx +7 -9
  38. package/ui/Layout/Sider.tsx +12 -6
  39. package/ui/Model/EditModal.tsx +4 -6
  40. package/ui/Model/EditWrapper.tsx +2 -5
  41. package/ui/Model/NewWrapper_Client.tsx +1 -2
  42. package/ui/Model/Remove.tsx +2 -1
  43. package/ui/Model/RemoveWrapper.tsx +2 -1
  44. package/ui/Model/SureToRemove.tsx +2 -5
  45. package/ui/Model/ViewEditModal.tsx +6 -9
  46. package/ui/Model/ViewModal.tsx +3 -4
  47. package/ui/Model/ViewWrapper.tsx +2 -1
  48. package/ui/ScreenNavigator.tsx +5 -5
  49. package/ui/System/SelectLanguage.tsx +2 -1
  50. package/ui/System/ThemeToggle.tsx +2 -1
  51. package/ui/Tab/Provider.tsx +6 -7
  52. package/ui/index.ts +1 -0
  53. package/vendor/use-agentic/AgentSession.ts +1 -2
  54. package/vendor/use-agentic/Agentic.tsx +4 -4
  55. package/vendor/use-agentic/AgenticSurface.ts +16 -10
  56. package/vendor/use-agentic/WIRE.md +0 -1
  57. package/vendor/use-agentic/types.ts +5 -6
  58. package/vendor/use-agentic/useAgentState.ts +0 -1
  59. package/vendor/use-agentic/useAgentTool.ts +3 -3
  60. package/webkit/bootCsr.tsx +10 -8
  61. package/webkit/usePageTool.tsx +5 -7
  62. package/store/agentic/readableValue.ts +0 -17
  63. package/store/agentic/useStExpose.ts +0 -18
  64. package/store/agentic/useStState.ts +0 -49
  65. package/types/store/agentic/readableValue.d.ts +0 -3
  66. package/types/store/agentic/useStExpose.d.ts +0 -8
  67. package/types/store/agentic/useStState.d.ts +0 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.52",
3
+ "version": "3.0.0-alpha.53",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -191,6 +191,7 @@ export class RouteTreeBuilder {
191
191
  parentLayouts: RouteRender[] = [],
192
192
  parentPaths: string[] = [],
193
193
  parentHead?: ResolveHead,
194
+ parentOverrides: RouteRender[] = [],
194
195
  ): PathRoute[] {
195
196
  const parentPath = parentPaths.filter((p) => p !== "/").join("");
196
197
  const currentPathSegment = /^\/\(.*\)$/.test(route.path) ? "" : route.path;
@@ -201,8 +202,10 @@ export class RouteTreeBuilder {
201
202
  const currentLayout = !isRoot && route.renderLayout ? route.renderLayout : null;
202
203
 
203
204
  const currentOverrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
204
- const renderRootLayouts = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
205
- const renderLayouts = [...parentLayouts, ...currentOverrideRenders, ...(currentLayout ? [currentLayout] : [])];
205
+ const overrideRenders = [...parentOverrides, ...currentOverrideRenders];
206
+ const rootLayoutStack = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
207
+ const renderRootLayouts = [...overrideRenders, ...rootLayoutStack];
208
+ const renderLayouts = [...parentLayouts, ...(currentLayout ? [currentLayout] : [])];
206
209
  if (route.renderLayout) {
207
210
  this.#fallbackRoutes.push({
208
211
  path: routePath,
@@ -213,11 +216,10 @@ export class RouteTreeBuilder {
213
216
  }
214
217
  const routeHead = RouteTreeBuilder.#composeHeadResolvers(route.renderLayout?.resolveHead, parentHead);
215
218
  const pageRenderRootLayouts =
216
- route.pageIncludesOwnLayout === false && currentRootLayout ? parentRootLayouts : renderRootLayouts;
217
- const pageRenderLayouts =
218
- route.pageIncludesOwnLayout === false && currentLayout
219
- ? [...parentLayouts, ...currentOverrideRenders]
220
- : renderLayouts;
219
+ route.pageIncludesOwnLayout === false && currentRootLayout
220
+ ? [...overrideRenders, ...parentRootLayouts]
221
+ : renderRootLayouts;
222
+ const pageRenderLayouts = route.pageIncludesOwnLayout === false && currentLayout ? parentLayouts : renderLayouts;
221
223
  const pageHead = route.pageIncludesOwnLayout === false ? parentHead : routeHead;
222
224
  return [
223
225
  ...(route.renderPage
@@ -236,7 +238,7 @@ export class RouteTreeBuilder {
236
238
  : []),
237
239
  ...(route.children.size
238
240
  ? [...route.children.values()].flatMap((child) =>
239
- this.#getPathRoutes(child, renderRootLayouts, renderLayouts, pathSegments, routeHead),
241
+ this.#getPathRoutes(child, rootLayoutStack, renderLayouts, pathSegments, routeHead, overrideRenders),
240
242
  )
241
243
  : []),
242
244
  ];
@@ -48,7 +48,6 @@ export interface AgentWireTool {
48
48
  name: string;
49
49
  description?: string;
50
50
  parameters?: Record<string, unknown>;
51
- effect?: string;
52
51
  needsConfirm?: boolean;
53
52
  }
54
53
 
@@ -56,7 +56,6 @@ export class StoreSurfaceSource implements SurfaceSource {
56
56
  required: ["path"],
57
57
  additionalProperties: false,
58
58
  },
59
- effect: "state",
60
59
 
61
60
  guard: (args) =>
62
61
  typeof args.path === "string" && args.path.startsWith("/") && !args.path.startsWith("//")
@@ -83,7 +82,6 @@ export class StoreSurfaceSource implements SurfaceSource {
83
82
  description:
84
83
  "Go back to the previous page in this session's history. Use it to undo a navigation; use navigate for a path.",
85
84
  parameters: { type: "object", properties: {}, additionalProperties: false },
86
- effect: "state",
87
85
 
88
86
  guard: () => (router.canGoBack() ? true : "There is no previous page in this session's history."),
89
87
  run: async () => {
@@ -112,7 +110,7 @@ export class StoreSurfaceSource implements SurfaceSource {
112
110
  },
113
111
  additionalProperties: false,
114
112
  },
115
- effect: "query",
113
+ settle: false,
116
114
  run: (args) => {
117
115
  const root = StoreSurfaceSource.#zoneRoot(viewKey);
118
116
  const section = typeof args.section === "string" ? args.section.trim() : "";
@@ -149,7 +147,6 @@ export class StoreSurfaceSource implements SurfaceSource {
149
147
  required: ["target"],
150
148
  additionalProperties: false,
151
149
  },
152
- effect: "state",
153
150
  run: (args) => {
154
151
  const name = typeof args.target === "string" ? args.target.trim() : "";
155
152
  if (!name) throw new Error("highlight needs a target.");
@@ -182,7 +179,6 @@ export class StoreSurfaceSource implements SurfaceSource {
182
179
  required: ["key"],
183
180
  additionalProperties: false,
184
181
  },
185
- effect: "state",
186
182
  run: (args: Record<string, unknown>) => {
187
183
  this.#bridge ??= AgentBridge.of();
188
184
  return this.#bridge.read(String(args.key), viewKey);
@@ -0,0 +1,112 @@
1
+ import {
2
+ type CLIENT_VALUE,
3
+ type Cls,
4
+ type Dayjs,
5
+ dayjs,
6
+ type EnumInstance,
7
+ type FIELD_META,
8
+ type GetStateObject,
9
+ isEnum,
10
+ PrimitiveRegistry,
11
+ type PrimitiveScalar,
12
+ type UnCls,
13
+ } from "akanjs/base";
14
+ import { type ConstantModelRef, type MaskModel, mask, maskFieldsOf } from "akanjs/constant";
15
+
16
+ type AgentSingleType = typeof PrimitiveScalar | EnumInstance<string, any> | ConstantModelRef;
17
+
18
+ /** What a readable declaration names its value as. One level of array, because a published value is one JSON shape. */
19
+ export type AgentFieldType = AgentSingleType | AgentSingleType[];
20
+
21
+ /**
22
+ * A model class resolves to its state object rather than its instance type, so a component may hand over either
23
+ * the hydrated document or the plain data copied out of one — masking reads the model that was named, not the
24
+ * class the value still carries.
25
+ */
26
+ export type AgentValueOf<T> = T extends readonly (infer F)[]
27
+ ? AgentValueOf<F>[]
28
+ : T extends { refName: "Any" }
29
+ ? unknown
30
+ : T extends EnumInstance<string, infer V>
31
+ ? V
32
+ : T extends DateConstructor
33
+ ? Dayjs | Date | string
34
+ : T extends { [FIELD_META]: unknown }
35
+ ? GetStateObject<UnCls<T>>
36
+ : T extends { [CLIENT_VALUE]: infer V }
37
+ ? V
38
+ : unknown;
39
+
40
+ type ValueKind = "any" | "date" | "scalar" | "enum" | "model";
41
+
42
+ /**
43
+ * Turns a declared type into what an agent may read of a value of that type.
44
+ *
45
+ * The type is the whole declaration: it typechecks what the component hands over, and it decides how the value is
46
+ * rendered — a model class masks by that model, a `Date` leaves as an ISO string, a scalar passes. `Any` is the
47
+ * escape hatch and passes the value untouched, so a payload nobody modeled stays publishable and the caller owns
48
+ * whether it is JSON and whether it is worth its tokens.
49
+ */
50
+ export class AgentValue {
51
+ static serialize(type: AgentFieldType, value: unknown): unknown {
52
+ const single = Array.isArray(type) ? type[0] : type;
53
+ if (Array.isArray(type) && Array.isArray(value)) return value.map((item) => AgentValue.#one(single, item));
54
+ return AgentValue.#one(single, value);
55
+ }
56
+
57
+ /**
58
+ * Reports an unreadable type the way `st.tool` reports an undescribable argument — on the console, and the
59
+ * declaration goes unpublished. Throwing would cost the route its server render over an agent-tooling mistake.
60
+ */
61
+ static publishable(owner: string, type: AgentFieldType): boolean {
62
+ try {
63
+ AgentValue.#kindOf(Array.isArray(type) ? type[0] : type);
64
+ return true;
65
+ } catch (error) {
66
+ console.error(`${owner} is not published: its type is ${error instanceof Error ? error.message : String(error)}`);
67
+ return false;
68
+ }
69
+ }
70
+
71
+ static #one(type: AgentSingleType, value: unknown): unknown {
72
+ if (value === null || value === undefined) return value;
73
+ switch (AgentValue.#kindOf(type)) {
74
+ case "date": {
75
+ const parsed = dayjs(value as string | number | Date);
76
+ return parsed.isValid() ? parsed.toISOString() : null;
77
+ }
78
+ case "model":
79
+ return mask(type as MaskModel, value);
80
+ default:
81
+ return value;
82
+ }
83
+ }
84
+
85
+ static #kindOf(type: AgentSingleType): ValueKind {
86
+ if (isEnum(type as Cls)) return "enum";
87
+ if (PrimitiveRegistry.has(type as unknown as Cls)) {
88
+ const refName = PrimitiveRegistry.getName(type as typeof PrimitiveScalar);
89
+ switch (refName) {
90
+ case "Any":
91
+ return "any";
92
+ case "Date":
93
+ return "date";
94
+ case "ID":
95
+ case "String":
96
+ case "Int":
97
+ case "Float":
98
+ case "Boolean":
99
+ return "scalar";
100
+ default:
101
+ throw new Error(`the scalar ${refName}, which an agent cannot read.`);
102
+ }
103
+ }
104
+ if (maskFieldsOf(type as MaskModel)) return "model";
105
+ throw new Error(`${AgentValue.#typeName(type)}, and a readable value is a scalar, an enum, a model, or Any.`);
106
+ }
107
+
108
+ static #typeName(type: AgentSingleType): string {
109
+ const named = type as { name?: string } | null | undefined;
110
+ return named?.name ? `the type ${named.name}` : `${String(type)}`;
111
+ }
112
+ }
@@ -0,0 +1,47 @@
1
+ import { useAgentResource } from "../../vendor/use-agentic";
2
+
3
+ import { useRef } from "../hooks";
4
+ import { type AgentFieldType, AgentValue, type AgentValueOf } from "./AgentValue";
5
+
6
+ export interface StExposeMeta {
7
+ /** `false` keeps the key out of post-call diff reports — for values that change on their own every second. */
8
+ report?: boolean;
9
+ }
10
+
11
+ /**
12
+ * A read-only value past its description, waiting for the value itself. `.value()` is the one hook.
13
+ *
14
+ * The declared type is what makes the read safe: it typechecks what the component hands over and it decides how
15
+ * the value is rendered, so a model's `hidden` and `secret` fields are stripped by the model that was named
16
+ * rather than by whatever class the value still happens to carry.
17
+ */
18
+ export class StExposeBuilder<T extends AgentFieldType> {
19
+ readonly #name: string | null;
20
+ readonly #type: T;
21
+ readonly #desc: string;
22
+ readonly #meta: StExposeMeta;
23
+
24
+ constructor(name: string | null, type: T, desc: string, meta: StExposeMeta = {}) {
25
+ this.#name = name;
26
+ this.#type = type;
27
+ this.#desc = desc;
28
+ this.#meta = meta;
29
+ }
30
+
31
+ /**
32
+ * A thunk is read when the agent reads, which is the difference that matters for a value assembled out of a ref
33
+ * the children fill in after this render — computing it here would publish whatever was there before they ran.
34
+ */
35
+ value(value: AgentValueOf<T> | (() => AgentValueOf<T>) | null | undefined): void {
36
+ const declared = useRef<{ name: string | null } | null>(null);
37
+ declared.current ??= {
38
+ name: this.#name && AgentValue.publishable(`st.expose("${this.#name}")`, this.#type) ? this.#name : null,
39
+ };
40
+ useAgentResource(declared.current.name, value, {
41
+ description: this.#desc,
42
+ report: this.#meta.report,
43
+ serialize: (current) =>
44
+ AgentValue.serialize(this.#type, typeof current === "function" ? (current as () => unknown)() : current),
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,24 @@
1
+ import type { AgentFieldType } from "./AgentValue";
2
+ import { StExposeBuilder, type StExposeMeta } from "./StExposeBuilder";
3
+
4
+ /**
5
+ * A readable value before it has said what it is: `st.expose("x", ID).desc("…")`.
6
+ *
7
+ * A falsy name declares nothing and publishes nothing, so a component whose publication is conditional keeps a
8
+ * constant hook count instead of branching around the chain.
9
+ */
10
+ export class StExposeDraft<T extends AgentFieldType> {
11
+ readonly #name: string | null;
12
+ readonly #type: T;
13
+ readonly #meta: StExposeMeta;
14
+
15
+ constructor(name: string | null, type: T, meta: StExposeMeta = {}) {
16
+ this.#name = name;
17
+ this.#type = type;
18
+ this.#meta = meta;
19
+ }
20
+
21
+ desc(text: string): StExposeBuilder<T> {
22
+ return new StExposeBuilder(this.#name, this.#type, text, this.#meta);
23
+ }
24
+ }
@@ -0,0 +1,85 @@
1
+ import { capitalize } from "akanjs/common";
2
+ import type { ParamFieldType } from "akanjs/constant";
3
+ import type { Dispatch, SetStateAction } from "react";
4
+ import { type JsonSchema, useAgentState } from "../../vendor/use-agentic";
5
+
6
+ import { useRef } from "../hooks";
7
+ import { type AgentFieldType, AgentValue, type AgentValueOf } from "./AgentValue";
8
+ import { StToolBuilder } from "./StToolBuilder";
9
+
10
+ export interface StStateMeta {
11
+ /** `false` keeps the key out of post-call diff reports — for values that change on their own every second. */
12
+ report?: boolean;
13
+ /** Publishes a `set<Name>` tool writing the type this state declares. Read-only without it. */
14
+ set?: boolean;
15
+ }
16
+
17
+ interface StStateDeclaration {
18
+ name: string | null;
19
+ set: { schema: JsonSchema; type: ParamFieldType } | null;
20
+ }
21
+
22
+ /**
23
+ * Local state past its description, waiting for its initial value. `.init()` is the one hook and returns what
24
+ * `useState` returns.
25
+ *
26
+ * The declared type does both halves: it renders the read, and — with `set` — it is the schema of the setter tool
27
+ * an agent writes through. A type nothing can describe costs the write, not the read and not the render.
28
+ */
29
+ export class StStateBuilder<T extends AgentFieldType> {
30
+ readonly #name: string | null;
31
+ readonly #type: T;
32
+ readonly #desc: string;
33
+ readonly #meta: StStateMeta;
34
+
35
+ constructor(name: string | null, type: T, desc: string, meta: StStateMeta = {}) {
36
+ this.#name = name;
37
+ this.#type = type;
38
+ this.#desc = desc;
39
+ this.#meta = meta;
40
+ }
41
+
42
+ init(
43
+ initial: AgentValueOf<T> | (() => AgentValueOf<T>),
44
+ ): [AgentValueOf<T>, Dispatch<SetStateAction<AgentValueOf<T>>>];
45
+ init(
46
+ initial: AgentValueOf<T> | null | (() => AgentValueOf<T> | null),
47
+ ): [AgentValueOf<T> | null, Dispatch<SetStateAction<AgentValueOf<T> | null>>];
48
+
49
+ init(initial: AgentValueOf<T> | null | (() => AgentValueOf<T> | null)): unknown {
50
+ type Value = AgentValueOf<T> | null;
51
+ const name = this.#name;
52
+ const type = this.#type;
53
+ const declared = useRef<StStateDeclaration | null>(null);
54
+ declared.current ??= {
55
+ name: name && AgentValue.publishable(`st.useState("${name}")`, type) ? name : null,
56
+ set: name && this.#meta.set ? StStateBuilder.#writable(name, type) : null,
57
+ };
58
+ const { set } = declared.current;
59
+ return useAgentState<Value>(declared.current.name, initial, {
60
+ description: this.#desc,
61
+ report: this.#meta.report,
62
+ serialize: (value) => AgentValue.serialize(type, value),
63
+ ...(set
64
+ ? {
65
+ set: set.schema,
66
+ parse: (value) =>
67
+ StToolBuilder.checkedValue(`set${capitalize(name ?? "")}`, "value", set.type, value) as Value,
68
+ }
69
+ : {}),
70
+ });
71
+ }
72
+
73
+ /** A `set` an agent could only call wrong leaves the key readable — the same trade `st.tool`'s `.arg` makes. */
74
+ static #writable(name: string, type: AgentFieldType): StStateDeclaration["set"] {
75
+ const scalar = type as unknown as ParamFieldType;
76
+ try {
77
+ return { schema: StToolBuilder.schemaOf(scalar), type: scalar };
78
+ } catch (error) {
79
+ console.error(
80
+ `st.useState("${name}") stays read-only: writing ${error instanceof Error ? error.message : String(error)}`,
81
+ );
82
+ return null;
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,23 @@
1
+ import type { AgentFieldType } from "./AgentValue";
2
+ import { StStateBuilder, type StStateMeta } from "./StStateBuilder";
3
+
4
+ /**
5
+ * Local state before it has said what it is: `st.useState("tab", String, { set: true }).desc("…")`.
6
+ *
7
+ * A falsy name keeps the state and publishes nothing, so a conditional surface never changes the hook count.
8
+ */
9
+ export class StStateDraft<T extends AgentFieldType> {
10
+ readonly #name: string | null;
11
+ readonly #type: T;
12
+ readonly #meta: StStateMeta;
13
+
14
+ constructor(name: string | null, type: T, meta: StStateMeta = {}) {
15
+ this.#name = name;
16
+ this.#type = type;
17
+ this.#meta = meta;
18
+ }
19
+
20
+ desc(text: string): StStateBuilder<T> {
21
+ return new StStateBuilder(this.#name, this.#type, text, this.#meta);
22
+ }
23
+ }
@@ -13,7 +13,6 @@ import {
13
13
  AgenticSurface,
14
14
  type JsonSchema,
15
15
  type ToolConfirm,
16
- type ToolEffect,
17
16
  type ToolGuard,
18
17
  useScopePath,
19
18
  useSurface,
@@ -23,15 +22,14 @@ import { tagAction } from "../actionTag";
23
22
  import { useEffect, useRef } from "../hooks";
24
23
 
25
24
  export interface StToolMeta {
26
- desc?: string;
27
- effect?: ToolEffect;
28
- confirm?: ToolConfirm;
29
- guard?: ToolGuard;
30
25
  /**
31
- * Set by a component that renders once per row. It is only true when the row's id rides in an argument rather
32
- * than in the closure, which is what makes every row's registration interchangeable.
26
+ * Whether the call has to be waited out before what it did to the screen is reported back to the model. `false`
27
+ * is a read that returns what is already there; the default waits, because a write may still be landing when
28
+ * `exec` resolves and a report taken then describes the screen one tick before the call.
33
29
  */
34
- shared?: boolean;
30
+ settle?: boolean;
31
+ confirm?: ToolConfirm;
32
+ guard?: ToolGuard;
35
33
  }
36
34
 
37
35
  interface StToolArg {
@@ -43,18 +41,19 @@ interface StToolArg {
43
41
 
44
42
  /** `oneOf` is the runtime half of `enumOf`: a value set only known once the component renders. */
45
43
  export interface StToolArgOption<V> {
46
- optional?: boolean;
47
44
  oneOf?: readonly V[];
48
45
  }
49
46
 
50
47
  type ArgValue<T> = T extends EnumInstance<string, infer V> ? V : T extends { [CLIENT_VALUE]: infer V } ? V : never;
51
48
 
52
49
  /**
53
- * A component tool in the signal's vocabulary: `st.tool("x", { desc }).arg("id", ID).exec(fn)`.
50
+ * A component tool past its description: `.arg()` for what the caller must pass, `.opt()` for what it may, and
51
+ * one `.exec()`.
54
52
  *
55
53
  * This is not A12's rejected store-action builder — a store action derives its schema from the endpoint it is
56
- * named after, while a component tool exists nowhere else, so declaring is the only source there is. `.arg()` only
57
- * accumulates data; `.exec()` is the one hook, so the chain must complete in one unconditional statement.
54
+ * named after, while a component tool exists nowhere else, so declaring is the only source there is. `.arg()` and
55
+ * `.opt()` only accumulate data; `.exec()` is the one hook, so the chain must complete in one unconditional
56
+ * statement.
58
57
  *
59
58
  * A falsy name declares the tool without publishing it: the callable still works for the click that a person
60
59
  * makes, and nothing reaches the agent. That is how a conditional surface stays writable at all — `.exec()` is a
@@ -62,21 +61,18 @@ type ArgValue<T> = T extends EnumInstance<string, infer V> ? V : T extends { [CL
62
61
  */
63
62
  export class StToolBuilder<Args extends unknown[] = []> {
64
63
  readonly #name: string | null;
64
+ readonly #desc: string;
65
65
  readonly #meta: StToolMeta;
66
66
  readonly #args: StToolArg[];
67
67
 
68
- constructor(name: string | null, meta: StToolMeta = {}, args: StToolArg[] = []) {
68
+ constructor(name: string | null, desc: string, meta: StToolMeta = {}, args: StToolArg[] = []) {
69
69
  this.#name = name;
70
+ this.#desc = desc;
70
71
  this.#meta = meta;
71
72
  this.#args = args;
72
73
  }
73
74
 
74
75
  arg<T extends ParamFieldType>(name: string, type: T): StToolBuilder<[...Args, ArgValue<T>]>;
75
- arg<T extends ParamFieldType, const V extends ArgValue<T>>(
76
- name: string,
77
- type: T,
78
- option: StToolArgOption<V> & { optional: true },
79
- ): StToolBuilder<[...Args, V | null]>;
80
76
  arg<T extends ParamFieldType, const V extends ArgValue<T>>(
81
77
  name: string,
82
78
  type: T,
@@ -86,12 +82,35 @@ export class StToolBuilder<Args extends unknown[] = []> {
86
82
  name: string,
87
83
  type: T,
88
84
  option: StToolArgOption<ArgValue<T>> = {},
85
+ ): StToolBuilder<[...Args, ArgValue<T>]> {
86
+ return this.#push(name, type, false, option) as StToolBuilder<[...Args, ArgValue<T>]>;
87
+ }
88
+
89
+ opt<T extends ParamFieldType>(name: string, type: T): StToolBuilder<[...Args, ArgValue<T> | null]>;
90
+ opt<T extends ParamFieldType, const V extends ArgValue<T>>(
91
+ name: string,
92
+ type: T,
93
+ option: StToolArgOption<V>,
94
+ ): StToolBuilder<[...Args, V | null]>;
95
+ opt<T extends ParamFieldType>(
96
+ name: string,
97
+ type: T,
98
+ option: StToolArgOption<ArgValue<T>> = {},
99
+ ): StToolBuilder<[...Args, ArgValue<T> | null]> {
100
+ return this.#push(name, type, true, option);
101
+ }
102
+
103
+ #push<T extends ParamFieldType>(
104
+ name: string,
105
+ type: T,
106
+ optional: boolean,
107
+ option: StToolArgOption<ArgValue<T>>,
89
108
  ): StToolBuilder<[...Args, ArgValue<T> | null]> {
90
109
 
91
110
  const published = this.#name && StToolBuilder.#describable(this.#name, name, type) ? this.#name : null;
92
- return new StToolBuilder(published, this.#meta, [
111
+ return new StToolBuilder(published, this.#desc, this.#meta, [
93
112
  ...this.#args,
94
- { name, type, optional: !!option.optional, ...(option.oneOf ? { oneOf: option.oneOf } : {}) },
113
+ { name, type, optional, ...(option.oneOf ? { oneOf: option.oneOf } : {}) },
95
114
  ]);
96
115
  }
97
116
 
@@ -101,8 +120,8 @@ export class StToolBuilder<Args extends unknown[] = []> {
101
120
  const scope = useScopePath();
102
121
  const live = useRef({ run, meta: this.#meta });
103
122
  live.current = { run, meta: this.#meta };
104
- const declared = useRef<{ name: string | null; meta: StToolMeta; args: StToolArg[] } | null>(null);
105
- declared.current ??= { name: this.#name, meta: this.#meta, args: this.#args };
123
+ const declared = useRef<{ name: string | null; desc: string; meta: StToolMeta; args: StToolArg[] } | null>(null);
124
+ declared.current ??= { name: this.#name, desc: this.#desc, meta: this.#meta, args: this.#args };
106
125
  const callable = useRef<((...args: Args) => Promise<void>) | null>(null);
107
126
  if (!callable.current) {
108
127
  const call = async (...args: Args) => {
@@ -118,9 +137,8 @@ export class StToolBuilder<Args extends unknown[] = []> {
118
137
  if (!spec || !name) return;
119
138
  return surface.registerTool(scope, {
120
139
  name,
121
- description: spec.meta.desc,
122
- effect: spec.meta.effect,
123
- ...(spec.meta.shared ? { shared: true } : {}),
140
+ description: spec.desc,
141
+ settle: spec.meta.settle,
124
142
  parameters: StToolBuilder.parametersOf(spec.args),
125
143
 
126
144
  ...(spec.meta.confirm === undefined && !name.startsWith("remove")
@@ -0,0 +1,22 @@
1
+ import { StToolBuilder, type StToolMeta } from "./StToolBuilder";
2
+
3
+ /**
4
+ * A component tool before it has said what it does: `st.tool("x", { confirm }).desc("…")`.
5
+ *
6
+ * The description is the step that has to happen, so it is the one the type insists on. A model picks a tool by
7
+ * that sentence and by nothing else, and the surface reads it a second time to tell fifty rows publishing one
8
+ * interchangeable verb from two components that happened to pick the same name.
9
+ */
10
+ export class StToolDraft {
11
+ readonly #name: string | null;
12
+ readonly #meta: StToolMeta;
13
+
14
+ constructor(name: string | null, meta: StToolMeta = {}) {
15
+ this.#name = name;
16
+ this.#meta = meta;
17
+ }
18
+
19
+ desc(text: string): StToolBuilder {
20
+ return new StToolBuilder(this.#name, text, this.#meta);
21
+ }
22
+ }
@@ -1,26 +1,32 @@
1
- import type { Dispatch, SetStateAction } from "react";
2
- import { StToolBuilder, type StToolMeta } from "./StToolBuilder";
3
- import { type StExposeMeta, useStExpose } from "./useStExpose";
4
- import { type StStateMeta, useStState } from "./useStState";
1
+ import type { AgentFieldType } from "./AgentValue";
2
+ import type { StExposeMeta } from "./StExposeBuilder";
3
+ import { StExposeDraft } from "./StExposeDraft";
4
+ import type { StStateMeta } from "./StStateBuilder";
5
+ import { StStateDraft } from "./StStateDraft";
6
+ import type { StToolMeta } from "./StToolBuilder";
7
+ import { StToolDraft } from "./StToolDraft";
5
8
 
6
9
  export interface StAgentic {
7
- /** Local state the in-page agent can read. Writes need a `set` type and go through a named setter tool. */
8
- useState: <T>(name: string | null, initial: T | (() => T), meta?: StStateMeta<T>) => [T, Dispatch<SetStateAction<T>>];
9
- /** A read-only derived value the agent can read while the component is mounted. */
10
- expose: (name: string | null, value: unknown, meta?: StExposeMeta) => void;
11
10
  /**
12
- * A component tool: `.arg("id", ID)` chained onto one `.exec()` hook.
11
+ * Local state the in-page agent can read: `.desc()` then `.init()`, which is the hook and returns what
12
+ * `useState` returns. Writes need `set: true` and go through the generated `set<Name>` tool.
13
+ */
14
+ useState: <T extends AgentFieldType>(name: string | null, type: T, meta?: StStateMeta) => StStateDraft<T>;
15
+ /** A read-only derived value the agent can read while the component is mounted: `.desc()` then `.value()`. */
16
+ expose: <T extends AgentFieldType>(name: string | null, type: T, meta?: StExposeMeta) => StExposeDraft<T>;
17
+ /**
18
+ * A component tool: `.desc()`, then `.arg()` / `.opt()`, chained onto one `.exec()` hook.
13
19
  *
14
20
  * A falsy name declares the tool without publishing it — the callable still drives the click a person makes.
15
- * Every one of these is a hook, so a conditional surface withholds the name rather than skipping the call.
21
+ * Every one of these ends in a hook, so a conditional surface withholds the name rather than skipping the chain.
16
22
  */
17
- tool: (name: string | null, meta?: StToolMeta) => StToolBuilder;
23
+ tool: (name: string | null, meta?: StToolMeta) => StToolDraft;
18
24
  }
19
25
 
20
26
  const stAgentic: StAgentic = {
21
- useState: useStState,
22
- expose: useStExpose,
23
- tool: (name, meta) => new StToolBuilder(name, meta),
27
+ useState: (name, type, meta) => new StStateDraft(name, type, meta),
28
+ expose: (name, type, meta) => new StExposeDraft(name, type, meta),
29
+ tool: (name, meta) => new StToolDraft(name, meta),
24
30
  };
25
31
 
26
32
  /** Idempotent — `StoreRegistry.build` runs once per merged root, always onto the one instance. */
@@ -1,9 +1,12 @@
1
+ export * from "./AgentValue";
1
2
  export * from "./attachAgentic";
2
3
  export * from "./formFields";
3
- export * from "./readableValue";
4
+ export * from "./StExposeBuilder";
5
+ export * from "./StExposeDraft";
6
+ export * from "./StStateBuilder";
7
+ export * from "./StStateDraft";
4
8
  export * from "./StToolBuilder";
9
+ export * from "./StToolDraft";
5
10
  export * from "./useFieldTool";
6
11
  export * from "./useFormTools";
7
12
  export * from "./useRelationFieldTool";
8
- export * from "./useStExpose";
9
- export * from "./useStState";
@@ -58,7 +58,6 @@ const rowEntries = (ref: FormFieldRef, arraySchema: JsonSchema): ToolEntry[] =>
58
58
  required: ["values"],
59
59
  additionalProperties: false,
60
60
  },
61
- effect: "state",
62
61
  run: (args) => {
63
62
  const checked = FormFields.checked(names.addFieldOnModel, "values", ref.field, args.values);
64
63
  return dispatcherOf(names.addFieldOnModel)?.(checked);
@@ -73,7 +72,6 @@ const rowEntries = (ref: FormFieldRef, arraySchema: JsonSchema): ToolEntry[] =>
73
72
  required: ["idxs"],
74
73
  additionalProperties: false,
75
74
  },
76
- effect: "state",
77
75
  guard: (args) => {
78
76
  const idxs = args.idxs;
79
77
  if (!Array.isArray(idxs) || !idxs.length) return `"idxs" of ${names.subFieldOnModel} takes at least one index.`;
@@ -118,7 +116,6 @@ const moveEntry = (ref: FormFieldRef, onChange: () => (value: unknown) => unknow
118
116
  required: ["from", "to"],
119
117
  additionalProperties: false,
120
118
  },
121
- effect: "state",
122
119
  guard: (args) => {
123
120
  const length = rowsOf(ref).length;
124
121
  const outside = ["from", "to"].filter((key) => {
@@ -168,7 +165,6 @@ export const useFieldTool = (onChange: unknown, { transform, disabled, sortable
168
165
  name: action,
169
166
  description: `Set ${ref.key} on the ${ref.refName} form.`,
170
167
  parameters: { type: "object", properties: { value: schema }, required: ["value"], additionalProperties: false },
171
- effect: "state",
172
168
  run: (args) => {
173
169
  const checked = FormFields.checked(action, "value", ref.field, args.value === undefined ? null : args.value);
174
170
  return (live.current.onChange as (value: unknown) => unknown)(normalized(checked, live.current.transform));