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.
- package/constant/fieldInfo.ts +11 -3
- package/constant/textFieldPaths.ts +13 -7
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/routeTreeBuilder.ts +10 -8
- package/service/predefinedAdaptor/llm.adaptor.ts +0 -1
- package/store/agent/StoreSurfaceSource.ts +1 -5
- package/store/agentic/AgentValue.ts +119 -0
- package/store/agentic/StExposeBuilder.ts +47 -0
- package/store/agentic/StExposeDraft.ts +24 -0
- package/store/agentic/StStateBuilder.ts +85 -0
- package/store/agentic/StStateDraft.ts +23 -0
- package/store/agentic/StToolBuilder.ts +43 -25
- package/store/agentic/StToolDraft.ts +22 -0
- package/store/agentic/attachAgentic.ts +20 -14
- package/store/agentic/index.ts +6 -3
- package/store/agentic/useFieldTool.ts +0 -4
- package/store/agentic/useFormTools.ts +4 -6
- package/store/agentic/useRelationFieldTool.ts +1 -2
- package/types/constant/fieldInfo.d.ts +10 -3
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +0 -1
- package/types/store/agentic/AgentValue.d.ts +43 -0
- package/types/store/agentic/StExposeBuilder.d.ts +21 -0
- package/types/store/agentic/StExposeDraft.d.ts +13 -0
- package/types/store/agentic/StStateBuilder.d.ts +21 -0
- package/types/store/agentic/StStateDraft.d.ts +12 -0
- package/types/store/agentic/StToolBuilder.d.ts +15 -16
- package/types/store/agentic/StToolDraft.d.ts +13 -0
- package/types/store/agentic/attachAgentic.d.ts +17 -11
- package/types/store/agentic/index.d.ts +6 -3
- package/types/store/agentic/useFormTools.d.ts +4 -4
- package/types/vendor/use-agentic/Agentic.d.ts +3 -3
- package/types/vendor/use-agentic/AgenticSurface.d.ts +5 -4
- package/types/vendor/use-agentic/types.d.ts +5 -5
- package/types/vendor/use-agentic/useAgentTool.d.ts +2 -2
- package/ui/Agent/Tool.tsx +1 -15
- package/ui/Data/ListContainer.tsx +20 -42
- package/ui/Dialog/Provider.tsx +7 -6
- package/ui/Dropdown.tsx +7 -9
- package/ui/Layout/Sider.tsx +12 -6
- package/ui/Model/EditModal.tsx +4 -6
- package/ui/Model/EditWrapper.tsx +2 -5
- package/ui/Model/NewWrapper_Client.tsx +1 -2
- package/ui/Model/Remove.tsx +2 -1
- package/ui/Model/RemoveWrapper.tsx +2 -1
- package/ui/Model/SureToRemove.tsx +2 -5
- package/ui/Model/ViewEditModal.tsx +6 -9
- package/ui/Model/ViewModal.tsx +3 -4
- package/ui/Model/ViewWrapper.tsx +2 -1
- package/ui/ScreenNavigator.tsx +5 -5
- package/ui/System/SelectLanguage.tsx +2 -1
- package/ui/System/ThemeToggle.tsx +2 -1
- package/ui/Tab/Provider.tsx +6 -7
- package/ui/index.ts +1 -0
- package/vendor/use-agentic/AgentSession.ts +1 -2
- package/vendor/use-agentic/Agentic.tsx +4 -4
- package/vendor/use-agentic/AgenticSurface.ts +16 -10
- package/vendor/use-agentic/WIRE.md +0 -1
- package/vendor/use-agentic/types.ts +5 -6
- package/vendor/use-agentic/useAgentState.ts +0 -1
- package/vendor/use-agentic/useAgentTool.ts +3 -3
- package/webkit/bootCsr.tsx +10 -8
- package/webkit/usePageTool.tsx +5 -7
- package/store/agentic/readableValue.ts +0 -17
- package/store/agentic/useStExpose.ts +0 -18
- package/store/agentic/useStState.ts +0 -49
- package/types/store/agentic/readableValue.d.ts +0 -3
- package/types/store/agentic/useStExpose.d.ts +0 -8
- package/types/store/agentic/useStState.d.ts +0 -12
package/constant/fieldInfo.ts
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
if (parent.fieldType === "
|
|
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
|
-
|
|
46
|
-
if (field.fieldType === "
|
|
47
|
-
|
|
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;
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -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
|
|
205
|
-
const
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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,
|
|
241
|
+
this.#getPathRoutes(child, rootLayoutStack, renderLayouts, pathSegments, routeHead, overrideRenders),
|
|
240
242
|
)
|
|
241
243
|
: []),
|
|
242
244
|
];
|
|
@@ -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
|
-
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
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
|
|
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()`
|
|
57
|
-
*
|
|
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
|
|
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.
|
|
122
|
-
|
|
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
|
+
}
|