akanjs 3.0.0-alpha.10 → 3.0.0-alpha.12

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 (164) hide show
  1. package/base/symbols.ts +4 -0
  2. package/common/index.ts +10 -0
  3. package/common/mcpExposure.ts +106 -0
  4. package/constant/immerify.ts +1 -1
  5. package/constant/index.ts +1 -0
  6. package/constant/mask.ts +60 -0
  7. package/dictionary/DictionaryLookup.ts +30 -0
  8. package/dictionary/dictInfo.ts +89 -1
  9. package/dictionary/index.ts +1 -0
  10. package/dictionary/locale.ts +16 -2
  11. package/document/index.ts +1 -0
  12. package/document/noDocumentError.ts +12 -0
  13. package/fetch/client/fetchClient.ts +12 -0
  14. package/fetch/fetchType/endpointFetch.type.ts +10 -4
  15. package/package.json +1 -1
  16. package/server/akanApp.ts +35 -1
  17. package/server/akanServer.ts +154 -5
  18. package/server/devtools/signalSerializer.ts +2 -2
  19. package/server/devtools/types.ts +2 -2
  20. package/server/mcp/McpAuth.ts +177 -0
  21. package/server/mcp/McpDispatcher.ts +245 -0
  22. package/server/mcp/McpEventStream.ts +76 -0
  23. package/server/mcp/McpExecutionContext.ts +105 -0
  24. package/server/mcp/McpRouter.ts +604 -0
  25. package/server/mcp/index.ts +5 -0
  26. package/server/resolver/database.resolver.ts +5 -4
  27. package/server/resolver/signal.resolver.ts +10 -0
  28. package/server/subRouteIndexDocument.tsx +61 -0
  29. package/server/systemPageDocument.tsx +2 -2
  30. package/server/systemPages.tsx +49 -16
  31. package/server/webRouter.ts +27 -1
  32. package/service/predefinedAdaptor/database.adaptor.ts +4 -3
  33. package/service/predefinedAdaptor/index.ts +1 -0
  34. package/service/predefinedAdaptor/insightQuery.ts +183 -0
  35. package/signal/agent/AgentCatalogue.ts +165 -0
  36. package/signal/agent/index.ts +1 -0
  37. package/signal/endpointInfo.ts +38 -2
  38. package/signal/guard.ts +11 -1
  39. package/signal/guards.ts +3 -1
  40. package/signal/index.ts +3 -0
  41. package/signal/mcp/McpDocument.ts +322 -0
  42. package/signal/mcp/McpProgress.ts +106 -0
  43. package/signal/mcp/McpUriTemplate.ts +85 -0
  44. package/signal/mcp/Msg.ts +368 -0
  45. package/signal/mcp/index.ts +5 -0
  46. package/signal/mcp/mcpProtocol.ts +120 -0
  47. package/signal/openapi/openapi.ts +32 -171
  48. package/signal/schema/JsonSchemaBuilder.ts +211 -0
  49. package/signal/schema/index.ts +1 -0
  50. package/signal/serializer/fetch.serializer.ts +3 -0
  51. package/signal/signalContext.ts +45 -5
  52. package/signal/slice.ts +15 -2
  53. package/signal/types.ts +33 -1
  54. package/store/action.ts +10 -21
  55. package/store/actionTag.ts +28 -0
  56. package/store/agent/AgentBridge.ts +281 -0
  57. package/store/agent/StoreCatalogue.ts +296 -0
  58. package/store/agent/index.ts +3 -0
  59. package/store/agent/types.ts +50 -0
  60. package/store/databaseStateNames.ts +31 -0
  61. package/store/formSetterNames.ts +21 -0
  62. package/store/index.ts +7 -0
  63. package/store/rootStore.ts +2 -1
  64. package/store/sliceRole.ts +36 -0
  65. package/store/state.ts +2 -12
  66. package/store/store.ts +5 -0
  67. package/store/storeInstance.ts +55 -17
  68. package/store/storeRegistry.ts +10 -0
  69. package/types/base/symbols.d.ts +4 -0
  70. package/types/common/index.d.ts +1 -0
  71. package/types/common/mcpExposure.d.ts +69 -0
  72. package/types/constant/index.d.ts +1 -0
  73. package/types/constant/mask.d.ts +34 -0
  74. package/types/dictionary/DictionaryLookup.d.ts +14 -0
  75. package/types/dictionary/base.dictionary.d.ts +1 -1
  76. package/types/dictionary/dictInfo.d.ts +50 -13
  77. package/types/dictionary/dictionary.d.ts +8 -8
  78. package/types/dictionary/index.d.ts +1 -0
  79. package/types/dictionary/locale.d.ts +9 -1
  80. package/types/document/index.d.ts +1 -0
  81. package/types/document/noDocumentError.d.ts +12 -0
  82. package/types/fetch/client/fetchClient.d.ts +9 -0
  83. package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
  84. package/types/server/akanApp.d.ts +6 -0
  85. package/types/server/akanServer.d.ts +43 -0
  86. package/types/server/devtools/types.d.ts +2 -2
  87. package/types/server/mcp/McpAuth.d.ts +62 -0
  88. package/types/server/mcp/McpDispatcher.d.ts +54 -0
  89. package/types/server/mcp/McpEventStream.d.ts +18 -0
  90. package/types/server/mcp/McpExecutionContext.d.ts +44 -0
  91. package/types/server/mcp/McpRouter.d.ts +68 -0
  92. package/types/server/mcp/index.d.ts +5 -0
  93. package/types/server/mcp.d.ts +1 -0
  94. package/types/server/subRouteIndexDocument.d.ts +8 -0
  95. package/types/server/systemPageDocument.d.ts +1 -0
  96. package/types/server/systemPages.d.ts +5 -0
  97. package/types/service/predefinedAdaptor/index.d.ts +1 -0
  98. package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
  99. package/types/signal/agent/AgentCatalogue.d.ts +100 -0
  100. package/types/signal/agent/index.d.ts +1 -0
  101. package/types/signal/agent.d.ts +1 -0
  102. package/types/signal/endpointInfo.d.ts +21 -3
  103. package/types/signal/guard.d.ts +10 -0
  104. package/types/signal/guards.d.ts +3 -1
  105. package/types/signal/index.d.ts +3 -0
  106. package/types/signal/mcp/McpDocument.d.ts +76 -0
  107. package/types/signal/mcp/McpProgress.d.ts +40 -0
  108. package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
  109. package/types/signal/mcp/Msg.d.ts +143 -0
  110. package/types/signal/mcp/index.d.ts +5 -0
  111. package/types/signal/mcp/mcpProtocol.d.ts +107 -0
  112. package/types/signal/mcp.d.ts +1 -0
  113. package/types/signal/openapi/openapi.d.ts +3 -3
  114. package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
  115. package/types/signal/schema/index.d.ts +1 -0
  116. package/types/signal/schema.d.ts +1 -0
  117. package/types/signal/signalContext.d.ts +16 -1
  118. package/types/signal/slice.d.ts +3 -1
  119. package/types/signal/types.d.ts +31 -1
  120. package/types/store/actionTag.d.ts +17 -0
  121. package/types/store/agent/AgentBridge.d.ts +70 -0
  122. package/types/store/agent/StoreCatalogue.d.ts +21 -0
  123. package/types/store/agent/index.d.ts +3 -0
  124. package/types/store/agent/types.d.ts +49 -0
  125. package/types/store/agent.d.ts +1 -0
  126. package/types/store/databaseStateNames.d.ts +25 -0
  127. package/types/store/formSetterNames.d.ts +16 -0
  128. package/types/store/index.d.ts +6 -0
  129. package/types/store/rootStore.d.ts +4 -1
  130. package/types/store/sliceRole.d.ts +25 -0
  131. package/types/store/store.d.ts +4 -1
  132. package/types/store/storeInstance.d.ts +16 -0
  133. package/types/store/storeRegistry.d.ts +3 -0
  134. package/types/ui/Agent/Dock.d.ts +16 -0
  135. package/types/ui/Agent/Section.d.ts +10 -0
  136. package/types/ui/Agent/StateKey.d.ts +15 -0
  137. package/types/ui/Agent/Tool.d.ts +15 -0
  138. package/types/ui/Agent/Transcript.d.ts +13 -0
  139. package/types/ui/Agent/index.d.ts +11 -0
  140. package/types/ui/Agent.d.ts +1 -0
  141. package/types/ui/Badge.d.ts +1 -1
  142. package/types/ui/Button.d.ts +1 -1
  143. package/types/ui/Signal/RestApi.d.ts +3 -2
  144. package/types/ui/Signal/style.d.ts +3 -0
  145. package/types/ui/agentAttrs.d.ts +14 -0
  146. package/types/ui/index.d.ts +2 -0
  147. package/ui/Agent/Dock.tsx +61 -0
  148. package/ui/Agent/Section.tsx +24 -0
  149. package/ui/Agent/StateKey.tsx +42 -0
  150. package/ui/Agent/Tool.tsx +66 -0
  151. package/ui/Agent/Transcript.tsx +33 -0
  152. package/ui/Agent/index.ts +7 -0
  153. package/ui/Badge.tsx +1 -1
  154. package/ui/Button.tsx +3 -1
  155. package/ui/DatePicker.tsx +1 -1
  156. package/ui/Field.tsx +15 -7
  157. package/ui/Input.tsx +7 -0
  158. package/ui/Select.tsx +2 -1
  159. package/ui/Signal/RestApi.tsx +57 -17
  160. package/ui/Signal/WebSocket.tsx +1 -1
  161. package/ui/Signal/style.ts +6 -1
  162. package/ui/Switch.tsx +2 -0
  163. package/ui/agentAttrs.ts +19 -0
  164. package/ui/index.ts +2 -0
package/base/symbols.ts CHANGED
@@ -13,6 +13,10 @@ export const STATE_META = Symbol.for("akan.state");
13
13
  export const STATE_INIT_META = Symbol.for("akan.state.init");
14
14
  export const STATE_DERIVED_META = Symbol.for("akan.state.derived");
15
15
  export const ACTION_META = Symbol.for("akan.action");
16
+ /** Which module declared each action, which is the dictionary node its words are written in. */
17
+ export const ACTION_OWNER_META = Symbol.for("akan.action.owner");
18
+ /** What a dispatcher does, carried on the function so a component handed one can annotate the DOM with it. */
19
+ export const ACTION_TAG = Symbol.for("akan.action.tag");
16
20
  export const SERVER_VALUE = Symbol.for("akan.value.server");
17
21
  export const CLIENT_VALUE = Symbol.for("akan.value.client");
18
22
  export const DEFAULT_VALUE = Symbol.for("akan.value.default");
package/common/index.ts CHANGED
@@ -27,6 +27,16 @@ export {
27
27
  resolveAkanI18nConfig,
28
28
  } from "./localeConfig";
29
29
  export { lowerlize } from "./lowerlize";
30
+ export {
31
+ isMcpDescribableArg,
32
+ type McpExposureEndpoint,
33
+ type McpExposureHints,
34
+ type McpExposureOption,
35
+ mcpBaseVerbOf,
36
+ mcpHintsOf,
37
+ mcpPromptRefusalOf,
38
+ mcpRefusalOf,
39
+ } from "./mcpExposure";
30
40
  export { mergeVersion } from "./mergeVersion";
31
41
  export { objectify } from "./objectify";
32
42
  export { pathGet } from "./pathGet";
@@ -0,0 +1,106 @@
1
+ import { capitalize } from "./capitalize";
2
+
3
+ /**
4
+ * What MCP says about one endpoint: whether it is published, why it is not, and the hints it carries.
5
+ *
6
+ * Structurally typed like the rest of `common/` so the same rules answer on both sides — the server builds its
7
+ * catalogue from them and the browser API explorer badges an endpoint from them. A second implementation would
8
+ * eventually disagree, and an audit surface that disagrees with the catalogue is worse than none.
9
+ *
10
+ * Every rejection returns the sentence an author reads, at boot in the server log and in the explorer. Fail-closed
11
+ * with no reason is what left a deliberate `mcp: { expose: true }` vanishing with nowhere to look but the source.
12
+ */
13
+ export interface McpExposureEndpoint {
14
+ type: string;
15
+ returns: { refName: string };
16
+ args: { name: string; refName: string; type: string; arrDepth?: number; nullable?: boolean }[];
17
+ guards?: string[];
18
+ fileUpload?: boolean;
19
+ }
20
+
21
+ export interface McpExposureOption {
22
+ /**
23
+ * The read-only deployment valve, which is server configuration. The browser explorer cannot know it and so
24
+ * badges what the code decided; the boot log is where a read-only deployment says what it dropped.
25
+ */
26
+ readOnly?: boolean;
27
+ }
28
+
29
+ /** The author's overrides for the hints derived below. */
30
+ export interface McpExposureHints {
31
+ readOnly?: boolean;
32
+ destructive?: boolean;
33
+ idempotent?: boolean;
34
+ }
35
+
36
+ /** `Any` publishes as the empty schema, which tells a model nothing — so it is left out rather than described. */
37
+ export const isMcpDescribableArg = (arg: { refName: string }) => arg.refName !== "Any";
38
+
39
+ /**
40
+ * Which slice-level verb opts in each CRUD endpoint a model generates. They carry no signal option of their own,
41
+ * so `mcp: { get: true }` on the slice is the only place their exposure can be written.
42
+ */
43
+ export const mcpBaseVerbOf = (refName: string, key: string) => {
44
+ const cap = capitalize(refName);
45
+ if (key === refName || key === `light${cap}`) return "get" as const;
46
+ if (key === `create${cap}`) return "create" as const;
47
+ if (key === `update${cap}`) return "update" as const;
48
+ if (key === `remove${cap}`) return "remove" as const;
49
+ return null;
50
+ };
51
+
52
+ /**
53
+ * The hints a client renders beside a tool. Hints only — clients are told to distrust them, so they inform a UI
54
+ * and never stand in for a guard. `openWorldHint` is always false: every endpoint reaches this app's own
55
+ * database, not the wider internet.
56
+ */
57
+ export const mcpHintsOf = (key: string, endpoint: { type: string }, mcp: McpExposureHints = {}) => {
58
+ const readOnly = mcp.readOnly ?? endpoint.type === "query";
59
+ const destructive = mcp.destructive ?? (!readOnly && /^(remove|delete)/.test(key));
60
+ return {
61
+ readOnlyHint: readOnly,
62
+ destructiveHint: destructive,
63
+ idempotentHint: mcp.idempotent ?? (readOnly || /^(set|update)/.test(key)),
64
+ openWorldHint: false,
65
+ };
66
+ };
67
+
68
+ /** The sentence explaining why this endpoint is not in the catalogue, or `null` when it is. */
69
+ export const mcpRefusalOf = (endpoint: McpExposureEndpoint, { readOnly }: McpExposureOption = {}): string | null => {
70
+ if (endpoint.type === "prompt") return mcpPromptRefusalOf(endpoint);
71
+ if (endpoint.type === "pubsub" || endpoint.type === "message")
72
+ return `\`${endpoint.type}\` rides the websocket, and its internal arguments read a socket an MCP request does not have.`;
73
+ if (readOnly && endpoint.type !== "query")
74
+ return "this deployment is read-only, which drops every endpoint that is not a query.";
75
+ if (endpoint.returns.refName === "Any" || endpoint.returns.refName === "Upload")
76
+ return `a return typed \`${endpoint.returns.refName}\` cannot be described to a model.`;
77
+ if (endpoint.fileUpload || endpoint.args.some((arg) => arg.refName === "Upload"))
78
+ return "a file upload has no MCP representation.";
79
+ if (endpoint.type === "mutation" && !endpoint.guards?.some((name) => name !== "Public"))
80
+ return "a mutation needs a real guard — `[Public]` is having none, spelled out.";
81
+ const opaque = endpoint.args.find((arg) => !isMcpDescribableArg(arg) && arg.type !== "search" && !arg.nullable);
82
+ if (opaque)
83
+ return `its required argument \`${opaque.name}\` is typed \`Any\`, which is left out of the published schema — expose a named filter slice instead.`;
84
+ return null;
85
+ };
86
+
87
+ /**
88
+ * A prompt is a read exposed on the same terms as a query, so every rejection here is one thing: an argument
89
+ * `prompts/get` cannot carry. Its `arguments` is a flat string map — one string per name, and no schema beside it
90
+ * — which rules out the argument *kinds* the builder already refuses and, just as surely, two argument *types* it
91
+ * accepts. A tool escapes both because it publishes a real JSON Schema.
92
+ */
93
+ export const mcpPromptRefusalOf = (endpoint: McpExposureEndpoint): string | null => {
94
+ const carried = endpoint.args.find((arg) => arg.type === "body" || arg.type === "msg" || arg.type === "room");
95
+ if (carried)
96
+ return `a prompt's arguments travel as a flat string map, so its \`${carried.type}\` argument \`${carried.name}\` cannot be carried.`;
97
+
98
+ const list = endpoint.args.find((arg) => arg.arrDepth);
99
+ if (list)
100
+ return `a prompt argument is one string, so its list argument \`${list.name}\` could never carry more than one value.`;
101
+
102
+ const opaque = endpoint.args.find((arg) => !isMcpDescribableArg(arg));
103
+ if (opaque)
104
+ return `its argument \`${opaque.name}\` is typed \`Any\`, and a prompt has no schema in which to describe one.`;
105
+ return null;
106
+ };
@@ -9,7 +9,7 @@ export const immerify = <T extends object>(modelRef: ConstantModelRef, objOrArr:
9
9
  }) as Record<string, unknown>;
10
10
  const objRecord = objOrArr as Record<string, unknown>;
11
11
  Object.entries(modelRef[FIELD_META]).forEach(([key, field]) => {
12
- if (field.isScalar && field.isClass && !!objRecord[key])
12
+ if (field.isScalar && field.isClass && objRecord[key])
13
13
  immeredObj[key] = immerify(field.modelRef, objRecord[key] as object);
14
14
  });
15
15
  return immeredObj as T;
package/constant/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./deserialize";
5
5
  export * from "./fieldInfo";
6
6
  export * from "./getDefault";
7
7
  export * from "./immerify";
8
+ export * from "./mask";
8
9
  export * from "./purify";
9
10
  export * from "./serialize";
10
11
  export * from "./textFieldPathSet";
@@ -0,0 +1,60 @@
1
+ import { FIELD_META } from "akanjs/base";
2
+
3
+ /**
4
+ * A model as masking reads it — the constructor, for the field metadata it carries at runtime.
5
+ *
6
+ * Structural rather than `ConstantModelRef` so that anything holding the class can name it, and read through
7
+ * `FIELD_META` the way `resolveReturn` reads it.
8
+ */
9
+ export interface MaskModel {
10
+ name: string;
11
+ }
12
+
13
+ /** The part of a field's metadata masking turns on. Mirrors what `resolveReturn` branches over. */
14
+ interface MaskField {
15
+ fieldType?: string;
16
+ isClass?: boolean;
17
+ modelRef?: MaskModel;
18
+ }
19
+
20
+ export const maskFieldsOf = (model: MaskModel): Record<string, MaskField> | null => {
21
+ const fields = (model as unknown as { [key: symbol]: unknown })[FIELD_META];
22
+ return fields && typeof fields === "object" ? (fields as Record<string, MaskField>) : null;
23
+ };
24
+
25
+ /** The `hidden` and `secret` field names of `model` that `value` still carries populated. */
26
+ export const leakingFieldsOf = (model: MaskModel, value: Record<string, unknown>): string[] => {
27
+ const fields = maskFieldsOf(model);
28
+ if (!fields) return [];
29
+ return Object.entries(fields)
30
+ .filter(([key, field]) => (field.fieldType === "hidden" || field.fieldType === "secret") && key in value)
31
+ .map(([key]) => key);
32
+ };
33
+
34
+ /**
35
+ * Strips what a model marks `hidden` or `secret`, by the model the caller names rather than by the one the value
36
+ * happens to still carry.
37
+ *
38
+ * That distinction is the whole point. A check that reads the class off the value can only mask what arrives as an
39
+ * instance, so a `{ ...doc }` spread, a `toJSON()`, an `immerify()`, or a round-trip through `JSON.stringify` reaches
40
+ * its destination with the metadata already gone and nothing can be done about it. A named model is metadata the
41
+ * value cannot lose, so a hydrated document and a plain object copied out of one mask identically.
42
+ *
43
+ * This is the field half of `resolveReturn` and deliberately not the whole of it. That one also loads every relation
44
+ * it walks past, which is right for a query's return value and wrong here, where the value is already in hand.
45
+ *
46
+ * Returns `unknown` rather than the argument's type, because what comes back is missing fields that type promises.
47
+ */
48
+ export const mask = (model: MaskModel, value: unknown): unknown => {
49
+ if (value === null || value === undefined || typeof value !== "object") return value;
50
+ if (Array.isArray(value)) return value.map((item: unknown) => mask(model, item));
51
+ const fields = maskFieldsOf(model);
52
+ if (!fields) return value;
53
+ const source = value as Record<string, unknown>;
54
+ const masked: Record<string, unknown> = {};
55
+ for (const [key, field] of Object.entries(fields)) {
56
+ if (field.fieldType === "hidden" || field.fieldType === "secret" || !(key in source)) continue;
57
+ masked[key] = field.isClass && field.modelRef ? mask(field.modelRef, source[key]) : source[key];
58
+ }
59
+ return masked;
60
+ };
@@ -0,0 +1,30 @@
1
+ import { pathGet } from "akanjs/common";
2
+ import { DictionaryRegistry } from "./dictionaryRegistry";
3
+ import type { DictionaryNode } from "./trans";
4
+
5
+ /**
6
+ * Reads translated text out of the merged dictionary tree by dotted key (`user.signal.createUser.arg.data.desc`).
7
+ *
8
+ * Unlike `translate`, a missing key resolves to `undefined` rather than echoing the key back: callers here are
9
+ * document generators (OpenAPI, MCP) that must omit an absent description instead of emitting `"user.signal.…"`
10
+ * as if it were prose. The merged root is snapshotted once per instance, so build a fresh one per document.
11
+ */
12
+ export class DictionaryLookup {
13
+ readonly language: string;
14
+ readonly #models: Record<string, DictionaryNode>;
15
+ constructor(language?: string) {
16
+ const root = DictionaryRegistry.getRoot();
17
+ this.language = language && root[language] ? language : (Object.keys(root).at(0) ?? "en");
18
+ this.#models = root[this.language] ?? {};
19
+ }
20
+
21
+ /** A bare refName names the model node itself, which holds no text of its own — its label is `<refName>.modelName`. */
22
+ text(key: string): string | undefined {
23
+ const [refName, ...rest] = key.split(".");
24
+ if (!refName) return undefined;
25
+ const model = this.#models[refName];
26
+ const node = (rest.length ? pathGet(rest.join("."), model) : model) as { t?: unknown } | null;
27
+ const text = node?.t;
28
+ return typeof text === "string" && text.length ? text : undefined;
29
+ }
30
+ }
@@ -56,6 +56,17 @@ type DictEndpointShape<Endpoint> =
56
56
  : Endpoint extends Record<string, EndpointInfo>
57
57
  ? EndpointCompactShape<Endpoint>
58
58
  : Record<never, never>;
59
+ /**
60
+ * The actions of a store, which is every method on it that dispatches.
61
+ *
62
+ * Derived from the shape rather than from a marker like `ENDPOINT_DICT_SHAPE`, because a store's custom actions
63
+ * are declared on the subclass — after `store()` has already returned, so there is nothing for it to stamp. What
64
+ * makes the shape readable anyway is that `st.do.<action>()` is typed `void`: an action returns nothing, so the
65
+ * void-returning methods are exactly the actions and `get` / `pick` / `slice` fall out on their own.
66
+ */
67
+ type DictStoreShape<Store> = {
68
+ [K in keyof Store as Store[K] extends (...args: never[]) => void | Promise<void> ? K & string : never]: true;
69
+ };
59
70
  type DictArgNames<ArgNames> = ArgNames extends readonly string[] ? ArgNames[number] : never;
60
71
  type DictFilterQuery<Filter> = DictFilterShape<Filter>["query"];
61
72
  type DictFilterSort<Filter> = DictFilterShape<Filter>["sort"];
@@ -140,6 +151,7 @@ export class ModelDictInfo<
140
151
  BaseSignalKey extends string = never,
141
152
  SliceKey extends string = "",
142
153
  EndpointKey extends string = never,
154
+ StoreKey extends string = never,
143
155
  ErrorKey extends string = never,
144
156
  EtcKey extends string = never,
145
157
  > {
@@ -245,6 +257,9 @@ export class ModelDictInfo<
245
257
  endpointDictionary: { [K in EndpointKey]: FunctionTranslation<Languages> } = {} as {
246
258
  [K in EndpointKey]: FunctionTranslation<Languages>;
247
259
  };
260
+ storeDictionary: { [K in StoreKey]: FieldTranslation<Languages> } = {} as {
261
+ [K in StoreKey]: FieldTranslation<Languages>;
262
+ };
248
263
  errorDictionary: { [K in ErrorKey]: Languages } = {} as {
249
264
  [K in ErrorKey]: Languages;
250
265
  };
@@ -278,6 +293,7 @@ export class ModelDictInfo<
278
293
  BaseSignalKey,
279
294
  SliceKey,
280
295
  EndpointKey,
296
+ StoreKey,
281
297
  ErrorKey,
282
298
  EtcKey
283
299
  >;
@@ -302,6 +318,7 @@ export class ModelDictInfo<
302
318
  BaseSignalKey,
303
319
  SliceKey,
304
320
  EndpointKey,
321
+ StoreKey,
305
322
  ErrorKey,
306
323
  EtcKey
307
324
  >;
@@ -327,6 +344,7 @@ export class ModelDictInfo<
327
344
  BaseSignalKey,
328
345
  SliceKey,
329
346
  EndpointKey,
347
+ StoreKey,
330
348
  ErrorKey,
331
349
  EtcKey
332
350
  >;
@@ -351,6 +369,7 @@ export class ModelDictInfo<
351
369
  BaseSignalKey,
352
370
  SliceKey,
353
371
  EndpointKey,
372
+ StoreKey,
354
373
  ErrorKey,
355
374
  EtcKey
356
375
  >;
@@ -375,6 +394,7 @@ export class ModelDictInfo<
375
394
  BaseSignalKey,
376
395
  SliceKey,
377
396
  EndpointKey,
397
+ StoreKey,
378
398
  ErrorKey,
379
399
  EtcKey
380
400
  >;
@@ -400,6 +420,7 @@ export class ModelDictInfo<
400
420
  BaseSignalKey,
401
421
  keyof DictSliceShape<Slice> & string,
402
422
  EndpointKey,
423
+ StoreKey,
403
424
  ErrorKey,
404
425
  EtcKey
405
426
  >;
@@ -425,6 +446,41 @@ export class ModelDictInfo<
425
446
  BaseSignalKey,
426
447
  SliceKey,
427
448
  keyof DictEndpointShape<Endpoint> & string,
449
+ StoreKey,
450
+ ErrorKey,
451
+ EtcKey
452
+ >;
453
+ }
454
+ /**
455
+ * What a store's custom actions are called, in the words a person would use.
456
+ *
457
+ * Optional, and the only stage that is: an action whose name matches the endpoint it wraps already reads as
458
+ * that endpoint's `.desc()`, which is most of them — the house naming rule (`st.do.X` reads the same as
459
+ * `fetch.X`) is what makes that true. This stage is for the rest, where inheriting the endpoint's words would
460
+ * be actively wrong: nine `getSummaryListIn*` actions that all call one endpoint, or `logout` over
461
+ * `signoutUser`, where the store name is the verb a user would say and the endpoint name is the verb the API
462
+ * has. `akan quality scan` names those and no others.
463
+ *
464
+ * Labels only, no `.arg()`. Parameter names are not in a class's type the way an endpoint builder's are, and an
465
+ * action mostly takes none anyway — its data comes from the form state the user already filled in.
466
+ */
467
+ store<Store>(
468
+ translate: (t: (trans: Languages) => FieldTranslation<Languages>) => Partial<{
469
+ [K in keyof DictStoreShape<Store>]: FieldTranslation<Languages>;
470
+ }>,
471
+ ) {
472
+ Object.assign(this.storeDictionary, translate(FieldTranslation.translate));
473
+ return this as unknown as ModelDictInfo<
474
+ Languages,
475
+ ModelKey,
476
+ InsightKey,
477
+ QueryKey,
478
+ SortKey,
479
+ EnumKey,
480
+ BaseSignalKey,
481
+ SliceKey,
482
+ EndpointKey,
483
+ StoreKey | (keyof DictStoreShape<Store> & string),
428
484
  ErrorKey,
429
485
  EtcKey
430
486
  >;
@@ -441,6 +497,7 @@ export class ModelDictInfo<
441
497
  BaseSignalKey,
442
498
  SliceKey,
443
499
  EndpointKey,
500
+ StoreKey,
444
501
  ErrorKey | (keyof ErrorDict & string),
445
502
  EtcKey
446
503
  >;
@@ -457,6 +514,7 @@ export class ModelDictInfo<
457
514
  BaseSignalKey,
458
515
  SliceKey,
459
516
  EndpointKey,
517
+ StoreKey,
460
518
  ErrorKey,
461
519
  EtcKey | (keyof EtcDict & string)
462
520
  >;
@@ -473,6 +531,7 @@ export class ModelDictInfo<
473
531
  GetBaseSignalKey<RefName>,
474
532
  SliceKey,
475
533
  EndpointKey,
534
+ StoreKey,
476
535
  ErrorKey,
477
536
  EtcKey
478
537
  >;
@@ -496,6 +555,7 @@ export class ModelDictInfo<
496
555
  this.#registerBaseSignalToRoot(refName, rootDict);
497
556
  this.#registerSliceToRoot(refName, rootDict);
498
557
  this.#registerEndpointToRoot(refName, rootDict);
558
+ this.#registerStoreToRoot(refName, rootDict);
499
559
  this.#registerErrorToRoot(refName, rootDict);
500
560
  this.#registerModelToRoot(refName, rootDict);
501
561
  this.#registerEtcToRoot(refName, rootDict);
@@ -777,6 +837,23 @@ export class ModelDictInfo<
777
837
  });
778
838
  });
779
839
  }
840
+ /** Under its own `store` node rather than beside `signal`, because the two hold the same key by design. */
841
+ #registerStoreToRoot(refName: string, rootDict: RootDictionary) {
842
+ this.languages.forEach((language) => {
843
+ ensureNode(getRootModelNode(rootDict, language, refName), "store");
844
+ });
845
+ Object.entries(this.storeDictionary as { [key: string]: FieldTranslation<Languages> }).forEach(([key, value]) => {
846
+ value.trans.forEach((t, idx) => {
847
+ ensureNode(ensureNode(getTranslatedRootModelNode(rootDict, this.languages, idx, refName), "store"), key).t = t;
848
+ });
849
+ value.descTrans?.forEach((t, idx) => {
850
+ ensureNode(
851
+ ensureNode(ensureNode(getTranslatedRootModelNode(rootDict, this.languages, idx, refName), "store"), key),
852
+ "desc",
853
+ ).t = t;
854
+ });
855
+ });
856
+ }
780
857
  #registerErrorToRoot(refName: string, rootDict: RootDictionary) {
781
858
  this.languages.forEach((language) => {
782
859
  ensureNode(getRootModelNode(rootDict, language, refName), "error");
@@ -803,7 +880,15 @@ export class ModelDictInfo<
803
880
  }
804
881
  }
805
882
 
806
- type AnyModelDictInfo = ModelDictInfo<any, any, any, any, any, any, any, any, any, any, any>;
883
+ /**
884
+ * Every parameter of `ModelDictInfo` is listed here positionally, so a parameter added to the class has to be
885
+ * added to all three lists below in the same slot. Omitting one does not fail to compile — inference silently
886
+ * shifts, so the last parameter falls off the end and becomes its default `never`: adding `StoreKey` before
887
+ * `ErrorKey` once cost an extending app the whole of the lib's `EtcKey` (`.translate()`) union, which reads at
888
+ * the call site as `l("<model>.<key>")` no longer existing.
889
+ */
890
+
891
+ type AnyModelDictInfo = ModelDictInfo<any, any, any, any, any, any, any, any, any, any, any, any>;
807
892
 
808
893
  type MergeTwoModelDicts<ModelDict1, ModelDict2> =
809
894
  ModelDict1 extends ModelDictInfo<
@@ -816,6 +901,7 @@ type MergeTwoModelDicts<ModelDict1, ModelDict2> =
816
901
  infer BaseSignalKey1,
817
902
  infer SliceKey1,
818
903
  infer EndpointKey1,
904
+ infer StoreKey1,
819
905
  infer ErrorKey1,
820
906
  infer EtcKey1
821
907
  >
@@ -829,6 +915,7 @@ type MergeTwoModelDicts<ModelDict1, ModelDict2> =
829
915
  infer BaseSignalKey2,
830
916
  infer SliceKey2,
831
917
  infer EndpointKey2,
918
+ infer StoreKey2,
832
919
  infer ErrorKey2,
833
920
  infer EtcKey2
834
921
  >
@@ -842,6 +929,7 @@ type MergeTwoModelDicts<ModelDict1, ModelDict2> =
842
929
  BaseSignalKey1 | BaseSignalKey2,
843
930
  SliceKey1 | SliceKey2,
844
931
  EndpointKey1 | EndpointKey2,
932
+ StoreKey1 | StoreKey2,
845
933
  ErrorKey1 | ErrorKey2,
846
934
  EtcKey1 | EtcKey2
847
935
  >
@@ -1,3 +1,4 @@
1
+ export * from "./DictionaryLookup";
1
2
  export * from "./dictInfo";
2
3
  export {
3
4
  __Dict_Key__,
@@ -168,6 +168,14 @@ export type ModelTrans<
168
168
  }>;
169
169
  error: { [K in ErrorKey]: Trans };
170
170
  } & { [K in EtcKey]: Trans };
171
+ /**
172
+ * Store action labels, keyed off the resolved `.store()` keys rather than off the store class.
173
+ *
174
+ * The endpoint half needs the class because it reads argument names out of it. A store entry is a label and a
175
+ * description with no arguments to name, so the keys the stage already resolved are the whole of it — which is
176
+ * also why a generated `dict.ts` passes no store type at all.
177
+ */
178
+ export type StoreTranslatorKey<T extends string, StoreKey extends string> = `${T}.store.${StoreKey}${"" | ".desc"}`;
171
179
  export type ModelTranslatorKey<T extends string, Model, Insight, Filter, Slice, Endpoint, EtcKey extends string> =
172
180
  | `${T}.modelName`
173
181
  | `${T}.modelDesc`
@@ -234,11 +242,14 @@ export const registerModelTrans = <
234
242
  infer _BaseSignalKey,
235
243
  infer _SliceKey,
236
244
  infer _EndpointKey,
245
+ infer StoreKey,
237
246
  infer ErrorKey,
238
247
  infer EtcKey
239
248
  >
240
249
  ? DictModule<
241
- ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey> | EnumTranslatorKey<EnumKey>,
250
+ | ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey>
251
+ | EnumTranslatorKey<EnumKey>
252
+ | StoreTranslatorKey<RefName, StoreKey>,
242
253
  `${RefName}.error.${ErrorKey}`
243
254
  >
244
255
  : never => {
@@ -252,11 +263,14 @@ export const registerModelTrans = <
252
263
  infer _BaseSignalKey,
253
264
  infer _SliceKey,
254
265
  infer _EndpointKey,
266
+ infer StoreKey,
255
267
  infer ErrorKey,
256
268
  infer EtcKey
257
269
  >
258
270
  ? DictModule<
259
- ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey> | EnumTranslatorKey<EnumKey>,
271
+ | ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey>
272
+ | EnumTranslatorKey<EnumKey>
273
+ | StoreTranslatorKey<RefName, StoreKey>,
260
274
  `${RefName}.error.${ErrorKey}`
261
275
  >
262
276
  : never;
package/document/index.ts CHANGED
@@ -7,5 +7,6 @@ export * from "./documentSchema";
7
7
  export * from "./filterMeta";
8
8
  export * from "./into";
9
9
  export * from "./loaderInfo";
10
+ export * from "./noDocumentError";
10
11
  export * from "./schema";
11
12
  export * from "./types";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A `pick` or a `get` that matched nothing. Typed so a caller can tell "you asked for something that is not
3
+ * there" from "we broke" — an agent-facing transport in particular must not log a stack and answer "the server
4
+ * failed" every time a model guesses an id.
5
+ *
6
+ * The message is unchanged from the bare `Error` this replaces, because callers already match on it, and the
7
+ * `statusCode` does not reach the HTTP layer: `isExceptionLike` wants a `toJSON` too, so a missing document
8
+ * stays a 500 there until someone decides framework-wide that it should be a 404.
9
+ */
10
+ export class NoDocumentError extends Error {
11
+ readonly statusCode = 404;
12
+ }
@@ -86,6 +86,15 @@ export class FetchClient {
86
86
  this.handler = this.#makeHandlerProxy();
87
87
  this.applySignal(serializedSignal);
88
88
  }
89
+ /**
90
+ * Every signal any client in this process has applied, which is the whole callable surface of the app.
91
+ *
92
+ * A copy, because this is the registry each client merges its own signals into and a reader that mutated it
93
+ * would change what the next client applies. Read by the agent catalogue, which needs the argument schemas.
94
+ */
95
+ static get sharedSerializedSignal(): { [key: string]: SerializedSignal } {
96
+ return { ...FetchClient.#sharedSerializedSignal };
97
+ }
89
98
  static resetSharedRegistry() {
90
99
  FetchClient.#sharedSerializedSignal = {};
91
100
  FetchClient.#sharedRegistryVersion++;
@@ -246,6 +255,8 @@ export class FetchClient {
246
255
  const parseReturn = this.#makeReturnParser(endpoint.returns);
247
256
  const { bodyArgs, uploadArgs } = FetchClient.classifyHttpArgs(endpoint.args);
248
257
  switch (endpoint.type) {
258
+
259
+ case "prompt":
249
260
  case "query": {
250
261
  const queryFn = async (...argData: unknown[]) => {
251
262
  const args = argData.slice(0, argLength);
@@ -286,6 +297,7 @@ export class FetchClient {
286
297
  }
287
298
  #registerEndpoint(key: string, endpoint: SerializedEndpoint, prefix?: string) {
288
299
  switch (endpoint.type) {
300
+ case "prompt":
289
301
  case "query": {
290
302
  this.#setHandlerFactory(key, () => this.#makeHttpFn(key, endpoint, prefix));
291
303
  return;
@@ -7,6 +7,7 @@ import type {
7
7
  EndpInfoReqType,
8
8
  EndpointCls,
9
9
  EndpointInfo,
10
+ PromptMessage,
10
11
  SlceCnstFull,
11
12
  SlceCnstInsight,
12
13
  SlceCnstLight,
@@ -40,6 +41,9 @@ type QueryOrMutationFetchFn<E, SlceCls extends SliceCls | never> = (
40
41
  ...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]
41
42
  ) => Promise<EndpInfoReturns<E, SlceCls>>;
42
43
 
44
+ /** Typed off `PromptResult` rather than the endpoint's return ref, which is the `Any` carrier a prompt rides on. */
45
+ type PromptFetchFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => Promise<PromptMessage[]>;
46
+
43
47
  type MessageEmitFn<E> = (...args: EndpInfoArgs<E>) => void;
44
48
 
45
49
  type MessageListenFn<E, SlceCls extends SliceCls | never> = (
@@ -58,12 +62,14 @@ type PubsubSubscribeFn<E, SlceCls extends SliceCls | never> = (
58
62
  type PrimaryFetchFn<E, SlceCls extends SliceCls | never> =
59
63
  EndpInfoReqType<E> extends "query" | "mutation"
60
64
  ? QueryOrMutationFetchFn<E, SlceCls>
61
- : EndpInfoReqType<E> extends "message"
62
- ? MessageEmitFn<E>
63
- : never;
65
+ : EndpInfoReqType<E> extends "prompt"
66
+ ? PromptFetchFn<E>
67
+ : EndpInfoReqType<E> extends "message"
68
+ ? MessageEmitFn<E>
69
+ : never;
64
70
 
65
71
  type PrimaryFetchType<EInfoObj extends { [key: string]: EndpointInfo }, SlceCls extends SliceCls | never> = {
66
- [K in keyof EInfoObj as EndpInfoReqType<EInfoObj[K]> extends "query" | "mutation" | "message"
72
+ [K in keyof EInfoObj as EndpInfoReqType<EInfoObj[K]> extends "query" | "mutation" | "prompt" | "message"
67
73
  ? K
68
74
  : never]: PrimaryFetchFn<EInfoObj[K], SlceCls>;
69
75
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.10",
3
+ "version": "3.0.0-alpha.12",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {