akanjs 3.0.0-alpha.52 → 3.0.0-alpha.54

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 (70) hide show
  1. package/constant/fieldInfo.ts +11 -3
  2. package/constant/textFieldPaths.ts +13 -7
  3. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  4. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  5. package/package.json +1 -1
  6. package/server/routeTreeBuilder.ts +10 -8
  7. package/service/predefinedAdaptor/llm.adaptor.ts +0 -1
  8. package/store/agent/StoreSurfaceSource.ts +1 -5
  9. package/store/agentic/AgentValue.ts +119 -0
  10. package/store/agentic/StExposeBuilder.ts +47 -0
  11. package/store/agentic/StExposeDraft.ts +24 -0
  12. package/store/agentic/StStateBuilder.ts +85 -0
  13. package/store/agentic/StStateDraft.ts +23 -0
  14. package/store/agentic/StToolBuilder.ts +43 -25
  15. package/store/agentic/StToolDraft.ts +22 -0
  16. package/store/agentic/attachAgentic.ts +20 -14
  17. package/store/agentic/index.ts +6 -3
  18. package/store/agentic/useFieldTool.ts +0 -4
  19. package/store/agentic/useFormTools.ts +4 -6
  20. package/store/agentic/useRelationFieldTool.ts +1 -2
  21. package/types/constant/fieldInfo.d.ts +10 -3
  22. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +0 -1
  23. package/types/store/agentic/AgentValue.d.ts +43 -0
  24. package/types/store/agentic/StExposeBuilder.d.ts +21 -0
  25. package/types/store/agentic/StExposeDraft.d.ts +13 -0
  26. package/types/store/agentic/StStateBuilder.d.ts +21 -0
  27. package/types/store/agentic/StStateDraft.d.ts +12 -0
  28. package/types/store/agentic/StToolBuilder.d.ts +15 -16
  29. package/types/store/agentic/StToolDraft.d.ts +13 -0
  30. package/types/store/agentic/attachAgentic.d.ts +17 -11
  31. package/types/store/agentic/index.d.ts +6 -3
  32. package/types/store/agentic/useFormTools.d.ts +4 -4
  33. package/types/vendor/use-agentic/Agentic.d.ts +3 -3
  34. package/types/vendor/use-agentic/AgenticSurface.d.ts +5 -4
  35. package/types/vendor/use-agentic/types.d.ts +5 -5
  36. package/types/vendor/use-agentic/useAgentTool.d.ts +2 -2
  37. package/ui/Agent/Tool.tsx +1 -15
  38. package/ui/Data/ListContainer.tsx +20 -42
  39. package/ui/Dialog/Provider.tsx +7 -6
  40. package/ui/Dropdown.tsx +7 -9
  41. package/ui/Layout/Sider.tsx +12 -6
  42. package/ui/Model/EditModal.tsx +4 -6
  43. package/ui/Model/EditWrapper.tsx +2 -5
  44. package/ui/Model/NewWrapper_Client.tsx +1 -2
  45. package/ui/Model/Remove.tsx +2 -1
  46. package/ui/Model/RemoveWrapper.tsx +2 -1
  47. package/ui/Model/SureToRemove.tsx +2 -5
  48. package/ui/Model/ViewEditModal.tsx +6 -9
  49. package/ui/Model/ViewModal.tsx +3 -4
  50. package/ui/Model/ViewWrapper.tsx +2 -1
  51. package/ui/ScreenNavigator.tsx +5 -5
  52. package/ui/System/SelectLanguage.tsx +2 -1
  53. package/ui/System/ThemeToggle.tsx +2 -1
  54. package/ui/Tab/Provider.tsx +6 -7
  55. package/ui/index.ts +1 -0
  56. package/vendor/use-agentic/AgentSession.ts +1 -2
  57. package/vendor/use-agentic/Agentic.tsx +4 -4
  58. package/vendor/use-agentic/AgenticSurface.ts +16 -10
  59. package/vendor/use-agentic/WIRE.md +0 -1
  60. package/vendor/use-agentic/types.ts +5 -6
  61. package/vendor/use-agentic/useAgentState.ts +0 -1
  62. package/vendor/use-agentic/useAgentTool.ts +3 -3
  63. package/webkit/bootCsr.tsx +10 -8
  64. package/webkit/usePageTool.tsx +5 -7
  65. package/store/agentic/readableValue.ts +0 -17
  66. package/store/agentic/useStExpose.ts +0 -18
  67. package/store/agentic/useStState.ts +0 -49
  68. package/types/store/agentic/readableValue.d.ts +0 -3
  69. package/types/store/agentic/useStExpose.d.ts +0 -8
  70. package/types/store/agentic/useStState.d.ts +0 -12
@@ -505,6 +505,14 @@ export interface FieldObject {
505
505
  [key: string]: ConstantField;
506
506
  }
507
507
 
508
+ /**
509
+ * `text` names a column in the plaintext search mirror, so a masked field carrying one would publish what it
510
+ * masks through search — the class build refuses it (`TextFieldPaths`). Removing the key from the option type
511
+ * moves that refusal to the call site, where the fix is obvious. Distributive because `FieldOption` is a union:
512
+ * a plain `Omit` over it would collapse to the keys the members share.
513
+ */
514
+ type WithoutTextRole<Option> = Option extends unknown ? Omit<Option, "text"> : never;
515
+
508
516
  type FieldOption<
509
517
  Value extends ConstantFieldTypeInput,
510
518
  MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never,
@@ -568,7 +576,7 @@ field.hidden = <
568
576
  MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never,
569
577
  >(
570
578
  value: Value,
571
- option: FieldOption<Value, MapValue> = {},
579
+ option: WithoutTextRole<FieldOption<Value, MapValue>> = {},
572
580
  ) =>
573
581
  new FieldInfo<"hidden", Value, ExplicitType, MapValue>(value, {
574
582
  ...option,
@@ -581,7 +589,7 @@ field.secret = <
581
589
  MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never,
582
590
  >(
583
591
  value: Value,
584
- option: FieldOption<Value, MapValue> = {},
592
+ option: WithoutTextRole<FieldOption<Value, MapValue>> = {},
585
593
  ) =>
586
594
  new FieldInfo<"secret", Value | null, ExplicitType | null, MapValue>(value, {
587
595
  ...option,
@@ -596,7 +604,7 @@ export const resolve = <
596
604
  MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never,
597
605
  >(
598
606
  value: Value,
599
- option: FieldOption<Value, MapValue> = {},
607
+ option: WithoutTextRole<FieldOption<Value, MapValue>> = {},
600
608
  ) =>
601
609
  new FieldInfo<"resolve", Value, ExplicitType, MapValue>(value, {
602
610
  ...option,
@@ -34,17 +34,23 @@ export class TextFieldPaths extends TextFieldPathSet {
34
34
  }
35
35
 
36
36
  #assertReachable(path: string, parent: ConstantField) {
37
- if (parent.fieldType === "secret") throw new Error(`Text field "${path}" is under a secret field`);
38
- if (parent.fieldType === "hidden") throw new Error(`Text field "${path}" is under a hidden field`);
39
- if (parent.fieldType === "resolve") throw new Error(`Text field "${path}" is under a resolved field`);
37
+
38
+ const fix = `Drop the text role on "${path}", or leave the parent unmasked.`;
39
+ if (parent.fieldType === "secret") throw new Error(`Text field "${path}" is under a secret field. ${fix}`);
40
+ if (parent.fieldType === "hidden") throw new Error(`Text field "${path}" is under a hidden field. ${fix}`);
41
+ if (parent.fieldType === "resolve") throw new Error(`Text field "${path}" is under a resolved field. ${fix}`);
40
42
  if (parent.arrDepth > 1) throw new Error(`Text field "${path}" is under a nested array and cannot be indexed`);
41
43
  }
42
44
 
43
45
  #assertIndexable(key: string, role: TextFieldRole, field: ConstantField) {
44
-
45
- if (field.fieldType === "secret") throw new Error(`Text field "${key}" is secret and must not be indexed`);
46
- if (field.fieldType === "hidden") throw new Error(`Text field "${key}" is hidden and must not be indexed`);
47
- if (field.fieldType === "resolve") throw new Error(`Text field "${key}" is resolved and is absent from _doc`);
46
+
47
+ const masked = `The search mirror stores plaintext. Drop the text role on "${key}", or make the field plain.`;
48
+ if (field.fieldType === "secret")
49
+ throw new Error(`Text field "${key}" is secret and must not be indexed. ${masked}`);
50
+ if (field.fieldType === "hidden")
51
+ throw new Error(`Text field "${key}" is hidden and must not be indexed. ${masked}`);
52
+ if (field.fieldType === "resolve")
53
+ throw new Error(`Text field "${key}" is resolved and is absent from _doc. Drop the text role on "${key}".`);
48
54
  if (field.isMap) throw new Error(`Text field "${key}" is a Map and cannot be indexed`);
49
55
  if (field.arrDepth > 1) throw new Error(`Text field "${key}" is a nested array and cannot be indexed`);
50
56
  const modelRef = field.modelRef as unknown as Cls;
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.54",
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,119 @@
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
+ * A scalar is recognised by `refName` and has to be matched before `FIELD_META`: `via.ts` augments the global
27
+ * `String`, `Boolean`, `Date` and `Map` constructors with `DatabaseConstantStatics`, so those four carry field
28
+ * metadata and would otherwise read as models. A model carries no `refName`, and `Map` — carrying neither — falls
29
+ * through to the model branch and is refused at declaration time instead.
30
+ */
31
+ export type AgentValueOf<T> = T extends readonly (infer F)[]
32
+ ? AgentValueOf<F>[]
33
+ : T extends { refName: "Any" }
34
+ ? unknown
35
+ : T extends EnumInstance<string, infer V>
36
+ ? V
37
+ : T extends DateConstructor
38
+ ? Dayjs | Date | string
39
+ : T extends { refName: string; [CLIENT_VALUE]: infer V }
40
+ ? V
41
+ : T extends { [FIELD_META]: unknown }
42
+ ? GetStateObject<UnCls<T>>
43
+ : T extends { [CLIENT_VALUE]: infer V }
44
+ ? V
45
+ : unknown;
46
+
47
+ type ValueKind = "any" | "date" | "scalar" | "enum" | "model";
48
+
49
+ /**
50
+ * Turns a declared type into what an agent may read of a value of that type.
51
+ *
52
+ * The type is the whole declaration: it typechecks what the component hands over, and it decides how the value is
53
+ * rendered — a model class masks by that model, a `Date` leaves as an ISO string, a scalar passes. `Any` is the
54
+ * escape hatch and passes the value untouched, so a payload nobody modeled stays publishable and the caller owns
55
+ * whether it is JSON and whether it is worth its tokens.
56
+ */
57
+ export class AgentValue {
58
+ static serialize(type: AgentFieldType, value: unknown): unknown {
59
+ const single = Array.isArray(type) ? type[0] : type;
60
+ if (Array.isArray(type) && Array.isArray(value)) return value.map((item) => AgentValue.#one(single, item));
61
+ return AgentValue.#one(single, value);
62
+ }
63
+
64
+ /**
65
+ * Reports an unreadable type the way `st.tool` reports an undescribable argument — on the console, and the
66
+ * declaration goes unpublished. Throwing would cost the route its server render over an agent-tooling mistake.
67
+ */
68
+ static publishable(owner: string, type: AgentFieldType): boolean {
69
+ try {
70
+ AgentValue.#kindOf(Array.isArray(type) ? type[0] : type);
71
+ return true;
72
+ } catch (error) {
73
+ console.error(`${owner} is not published: its type is ${error instanceof Error ? error.message : String(error)}`);
74
+ return false;
75
+ }
76
+ }
77
+
78
+ static #one(type: AgentSingleType, value: unknown): unknown {
79
+ if (value === null || value === undefined) return value;
80
+ switch (AgentValue.#kindOf(type)) {
81
+ case "date": {
82
+ const parsed = dayjs(value as string | number | Date);
83
+ return parsed.isValid() ? parsed.toISOString() : null;
84
+ }
85
+ case "model":
86
+ return mask(type as MaskModel, value);
87
+ default:
88
+ return value;
89
+ }
90
+ }
91
+
92
+ static #kindOf(type: AgentSingleType): ValueKind {
93
+ if (isEnum(type as Cls)) return "enum";
94
+ if (PrimitiveRegistry.has(type as unknown as Cls)) {
95
+ const refName = PrimitiveRegistry.getName(type as typeof PrimitiveScalar);
96
+ switch (refName) {
97
+ case "Any":
98
+ return "any";
99
+ case "Date":
100
+ return "date";
101
+ case "ID":
102
+ case "String":
103
+ case "Int":
104
+ case "Float":
105
+ case "Boolean":
106
+ return "scalar";
107
+ default:
108
+ throw new Error(`the scalar ${refName}, which an agent cannot read.`);
109
+ }
110
+ }
111
+ if (maskFieldsOf(type as MaskModel)) return "model";
112
+ throw new Error(`${AgentValue.#typeName(type)}, and a readable value is a scalar, an enum, a model, or Any.`);
113
+ }
114
+
115
+ static #typeName(type: AgentSingleType): string {
116
+ const named = type as { name?: string } | null | undefined;
117
+ return named?.name ? `the type ${named.name}` : `${String(type)}`;
118
+ }
119
+ }
@@ -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
+ }