akanjs 3.0.0-alpha.17 → 3.0.0-alpha.19

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.
@@ -6,8 +6,8 @@
6
6
  export const labelOf = (model: unknown, value: unknown): string | undefined => {
7
7
  if (!value || typeof value !== "object") return undefined;
8
8
  const source = value as Record<string, unknown>;
9
- const paths = (model as { text?: { title?: readonly string[] } } | null)?.text?.title;
10
- const titlePath = paths?.find((path) => !path.includes(".") && !path.includes("["));
9
+ const paths = (model as { text?: { title?: Iterable<string> } } | null)?.text?.title;
10
+ const titlePath = [...(paths ?? [])].find((path) => !path.includes(".") && !path.includes("["));
11
11
  for (const key of [titlePath, "title", "name"]) {
12
12
  if (!key) continue;
13
13
  const candidate = source[key];
@@ -10,6 +10,7 @@ import type {
10
10
  SerializedSlice,
11
11
  ServiceSignal,
12
12
  } from "akanjs/signal";
13
+ import { agentTurnConstant } from "../agentTurn";
13
14
  import type { ClientSignal, FetchClientType, FetchSignalInput, MergeAllFetchTypes, SliceMeta } from "../fetchType";
14
15
  import { memoizeRequestQuery, cookies as requestCookies, headers as requestHeaders } from "../requestStorage";
15
16
  import type { GetSliceMetaObjFromDatabaseSignals } from "../types";
@@ -47,6 +48,19 @@ const globalWithSharedClient = globalThis as typeof globalThis & { [SHARED_CLIEN
47
48
  const sharedClientState: SharedClientState = globalWithSharedClient[SHARED_CLIENT_KEY] ?? { proxy: null, origin: null };
48
49
  globalWithSharedClient[SHARED_CLIENT_KEY] = sharedClientState;
49
50
 
51
+ interface SharedSignalRegistry {
52
+ signal: { [key: string]: SerializedSignal };
53
+ version: number;
54
+ }
55
+
56
+ const SHARED_SIGNAL_KEY = Symbol.for("akanjs.fetch.sharedSignalRegistry");
57
+ const globalWithSharedSignal = globalThis as typeof globalThis & { [SHARED_SIGNAL_KEY]?: SharedSignalRegistry };
58
+ const sharedSignalRegistry: SharedSignalRegistry = globalWithSharedSignal[SHARED_SIGNAL_KEY] ?? {
59
+ signal: {},
60
+ version: 0,
61
+ };
62
+ globalWithSharedSignal[SHARED_SIGNAL_KEY] = sharedSignalRegistry;
63
+
50
64
  type ClientSignalMap<SigType extends { fetch: any }> = {
51
65
  [K in keyof SigType as SigType[K] extends DatabaseSignal<any, any, any, any>
52
66
  ? K
@@ -57,8 +71,9 @@ type ClientSignalMap<SigType extends { fetch: any }> = {
57
71
 
58
72
  /** Runtime fetch client that registers serialized Akan signals as HTTP/WebSocket methods. */
59
73
  export class FetchClient {
60
- static #sharedSerializedSignal: { [key: string]: SerializedSignal } = {};
61
- static #sharedRegistryVersion = 0;
74
+ static {
75
+ ConstantRegistry.setScalar(agentTurnConstant.refName, agentTurnConstant);
76
+ }
62
77
  readonly logger = new Logger("FetchClient");
63
78
  readonly origin: string;
64
79
  readonly http: HttpClient;
@@ -93,11 +108,11 @@ export class FetchClient {
93
108
  * would change what the next client applies. Read by the agent catalogue, which needs the argument schemas.
94
109
  */
95
110
  static get sharedSerializedSignal(): { [key: string]: SerializedSignal } {
96
- return { ...FetchClient.#sharedSerializedSignal };
111
+ return { ...sharedSignalRegistry.signal };
97
112
  }
98
113
  static resetSharedRegistry() {
99
- FetchClient.#sharedSerializedSignal = {};
100
- FetchClient.#sharedRegistryVersion++;
114
+ sharedSignalRegistry.signal = {};
115
+ sharedSignalRegistry.version++;
101
116
  }
102
117
  static resetSharedClient() {
103
118
  sharedClientState.proxy = null;
@@ -147,9 +162,9 @@ export class FetchClient {
147
162
  applySignal(serializedSignal: { [key: string]: SerializedSignal }, { share = true }: { share?: boolean } = {}) {
148
163
  if (share && Object.keys(serializedSignal).length > 0) {
149
164
  for (const [refName, signal] of Object.entries(serializedSignal))
150
- FetchClient.#mergeSerializedSignalInto(FetchClient.#sharedSerializedSignal, refName, signal);
151
- FetchClient.#sharedRegistryVersion++;
152
- this.#sharedRegistryAppliedVersion = FetchClient.#sharedRegistryVersion;
165
+ FetchClient.#mergeSerializedSignalInto(sharedSignalRegistry.signal, refName, signal);
166
+ sharedSignalRegistry.version++;
167
+ this.#sharedRegistryAppliedVersion = sharedSignalRegistry.version;
153
168
  }
154
169
  for (const [refName, signal] of Object.entries(serializedSignal))
155
170
  FetchClient.#mergeSerializedSignalInto(this.serializedSignal, refName, signal);
@@ -183,9 +198,9 @@ export class FetchClient {
183
198
  });
184
199
  }
185
200
  #syncSharedRegistry() {
186
- if (this.#sharedRegistryAppliedVersion === FetchClient.#sharedRegistryVersion) return;
187
- this.applySignal(FetchClient.#sharedSerializedSignal, { share: false });
188
- this.#sharedRegistryAppliedVersion = FetchClient.#sharedRegistryVersion;
201
+ if (this.#sharedRegistryAppliedVersion === sharedSignalRegistry.version) return;
202
+ this.applySignal(sharedSignalRegistry.signal, { share: false });
203
+ this.#sharedRegistryAppliedVersion = sharedSignalRegistry.version;
189
204
  }
190
205
  #getOrCreateHandler(key: string): FetchHandler | undefined {
191
206
  const current = this.#handlerStore[key];
@@ -719,9 +734,16 @@ export class FetchClient {
719
734
  });
720
735
  }
721
736
  });
722
- const instance = new FetchClient(getEnv().serverHttpUri, handler, serializedSignal);
737
+ const instance = new FetchClient(FetchClient.#originFromEnv(), handler, serializedSignal);
723
738
  return FetchClient.#makeProxy<MergeAllFetchTypes<Signals>, GetSliceMetaObjFromDatabaseSignals<Signals>>(instance);
724
739
  }
740
+ static #originFromEnv() {
741
+ try {
742
+ return getEnv().serverHttpUri;
743
+ } catch {
744
+ return "";
745
+ }
746
+ }
725
747
  static build<SigType extends { fetch: any }>(
726
748
  constant: object,
727
749
  serializedSignal: { [key: string]: SerializedSignal },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.17",
3
+ "version": "3.0.0-alpha.19",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -13,6 +13,7 @@ import {
13
13
  getRequestFrameState,
14
14
  getRequestPolicy,
15
15
  getRequestTheme,
16
+ pushRequestFallback,
16
17
  requestStorage,
17
18
  setRequestFrameState,
18
19
  untrackedCookies,
@@ -1182,8 +1183,11 @@ export class RscRenderer {
1182
1183
  }
1183
1184
 
1184
1185
  #runWithRequest<T>(request: Request, fn: () => Promise<T>): Promise<T> {
1185
- if (requestStorage) return Promise.resolve(requestStorage.run(request, fn));
1186
- return fn();
1186
+
1187
+ const cleanup = pushRequestFallback(request);
1188
+ const run = () => Promise.resolve(fn()).finally(() => cleanup());
1189
+ if (requestStorage) return Promise.resolve(requestStorage.run(request, run));
1190
+ return run();
1187
1191
  }
1188
1192
 
1189
1193
  async #renderFallbackDocument({
@@ -0,0 +1 @@
1
+ export * from "akanjs/fetch";
@@ -32,7 +32,7 @@ export class DeepseekLlm
32
32
  implements LlmAdaptor
33
33
  {
34
34
  get #model() {
35
- return this.llmOption.model ?? "deepseek-chat";
35
+ return this.llmOption.model ?? "deepseek-v4-flash";
36
36
  }
37
37
  get #host() {
38
38
  return this.llmOption.host ?? "https://api.deepseek.com";
@@ -83,35 +83,54 @@ export class ScreenReader {
83
83
  if (el.hasAttribute("data-agent-ui") || el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true")
84
84
  return;
85
85
  if (typeof el.checkVisibility === "function" && !el.checkVisibility()) return;
86
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return this.#control(el, tag);
86
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") {
87
+ this.#control(el, tag);
88
+ return;
89
+ }
87
90
  if (tag === "IMG") {
88
91
  const alt = el.getAttribute("alt");
89
92
  if (alt) this.#buffer += ` [image: ${alt}]`;
90
93
  return;
91
94
  }
92
- if (tag === "BR") return this.#flush();
93
- if (tag === "PRE") return this.#pre(el);
94
- if (tag === "A") return this.#anchor(el);
95
- if (tag === "BUTTON" || el.getAttribute("role") === "button") return this.#button(el);
95
+ if (tag === "BR") {
96
+ this.#flush();
97
+ return;
98
+ }
99
+ if (tag === "PRE") {
100
+ this.#pre(el);
101
+ return;
102
+ }
103
+ if (tag === "A") {
104
+ this.#anchor(el);
105
+ return;
106
+ }
107
+ if (tag === "BUTTON" || el.getAttribute("role") === "button") {
108
+ this.#button(el);
109
+ return;
110
+ }
96
111
  const level = headingLevels[tag as keyof typeof headingLevels];
97
112
  if (level) {
98
113
  this.#flush();
99
114
  this.#walkChildren(el);
100
- return this.#flush(`${"#".repeat(level)} `);
115
+ this.#flush(`${"#".repeat(level)} `);
116
+ return;
101
117
  }
102
118
  if (tag === "LI") {
103
119
  this.#flush();
104
120
  this.#walkChildren(el);
105
- return this.#flush("- ");
121
+ this.#flush("- ");
122
+ return;
106
123
  }
107
124
  if (tag === "TD" || tag === "TH") {
108
125
  if (this.#buffer.trim()) this.#buffer += " |";
109
- return this.#walkChildren(el);
126
+ this.#walkChildren(el);
127
+ return;
110
128
  }
111
129
  if (blockTags.has(tag)) {
112
130
  this.#flush();
113
131
  this.#walkChildren(el);
114
- return this.#flush();
132
+ this.#flush();
133
+ return;
115
134
  }
116
135
  this.#walkChildren(el);
117
136
  }
@@ -1,5 +1,5 @@
1
1
  import { type Cls, FIELD_META, PrimitiveRegistry, type PrimitiveScalar } from "akanjs/base";
2
- import { capitalize } from "akanjs/common";
2
+ import { capitalize, Logger } from "akanjs/common";
3
3
  import { ConstantRegistry } from "akanjs/constant";
4
4
  import type { AgentRefusal, SerializedArg, SerializedSignal } from "akanjs/signal";
5
5
 
@@ -71,7 +71,10 @@ export class StoreCatalogue {
71
71
  readonly #instance: StoreInstance;
72
72
  readonly #endpoints = new Map<string, { endpoint: SerializedSignal["endpoint"][string]; refName: string }>();
73
73
  readonly #refused = new Set<string>();
74
+ readonly #baseFieldSetters = new Set<string>();
74
75
  #formSetterCache: Map<string, SerializedStoreAction> | null = null;
76
+ static readonly #baseDocumentFields = new Set(["id", "createdAt", "updatedAt", "removedAt"]);
77
+ static readonly #warnedMismatches = new Set<string>();
75
78
 
76
79
  constructor(instance: StoreInstance, serializedSignal: Record<string, SerializedSignal>) {
77
80
  this.#instance = instance;
@@ -154,9 +157,12 @@ export class StoreCatalogue {
154
157
  if (this.#refused.has(key)) return null;
155
158
 
156
159
  if (this.#instance.generatedSetters.has(key)) return null;
160
+
161
+ if (this.#baseFieldSetters.has(key)) return null;
157
162
  if (!this.visibility.visibleAction(key, this.#instance.actionOwners.get(key)?.refName)) return null;
158
163
  const endpoint = this.#endpoints.get(key);
159
- if (endpoint) return [key, this.#endpointAction(key, endpoint)];
164
+ if (endpoint)
165
+ return this.#actionFitsEndpoint(key, endpoint.endpoint) ? [key, this.#endpointAction(key, endpoint)] : null;
160
166
  const formSetter = this.#formSetterCache?.get(key);
161
167
  if (formSetter) return [key, formSetter];
162
168
  const slice = this.#sliceAction(key);
@@ -165,9 +171,33 @@ export class StoreCatalogue {
165
171
  }
166
172
 
167
173
  /**
168
- * An action named after an endpoint takes the endpoint's arguments. That is not a coincidence to be verified but
169
- * the house naming rule — "the signal, store, and dictionary re-add the model, so `st.do.X` reads the same as
170
- * `fetch.X`" and it is where the store's schemas come from for free.
174
+ * The borrowed schema is only honest when the action can consume it: one that declares fewer parameters than the
175
+ * endpoint silently drops the tail and reads stale form state instead, so a schema-correct call becomes a wrong
176
+ * mutation reported as success. More parameters than the endpoint is the generated shape
177
+ * `create<Model>(data, options?)` — and stays published. `Function.length` stops at the first default, so a
178
+ * defaulted mirror parameter reads as missing here; align it with the endpoint or exclude the action.
179
+ */
180
+ #actionFitsEndpoint(key: string, endpoint: SerializedSignal["endpoint"][string]) {
181
+ const arity = this.#instance.actionArity.get(key) ?? 0;
182
+ const declared = endpoint.args.length;
183
+ if (arity >= declared) return true;
184
+ this.#refuse(
185
+ key,
186
+ `it declares ${arity} parameter${arity === 1 ? "" : "s"} while the same-named endpoint takes ${declared} — a schema-shaped call would drop the tail and read form state instead. Align the action's parameters with the endpoint, or exclude the action.`,
187
+ );
188
+ if (!StoreCatalogue.#warnedMismatches.has(key)) {
189
+ StoreCatalogue.#warnedMismatches.add(key);
190
+ Logger.warn(
191
+ `st.do.${key} does not mirror endpoint ${key} (${arity} vs ${declared} args) — not published to agents.`,
192
+ );
193
+ }
194
+ return false;
195
+ }
196
+
197
+ /**
198
+ * An action named after an endpoint takes the endpoint's arguments — the house naming rule ("the signal, store,
199
+ * and dictionary re-add the model, so `st.do.X` reads the same as `fetch.X`") is where the store's schemas come
200
+ * from for free, and #actionFitsEndpoint is what holds the rule to its word.
171
201
  */
172
202
  #endpointAction(
173
203
  key: string,
@@ -193,6 +223,10 @@ export class StoreCatalogue {
193
223
  if (!fields) continue;
194
224
  for (const [field, meta] of Object.entries(fields)) {
195
225
  const names = formSetterNames(className, field);
226
+ if (StoreCatalogue.#baseDocumentFields.has(field)) {
227
+ for (const name of Object.values(names)) if (name in this.#instance.do) this.#baseFieldSetters.add(name);
228
+ continue;
229
+ }
196
230
  if (!(names.setFieldOnModel in this.#instance.do)) continue;
197
231
  if (names.uploadFieldOnModel in this.#instance.do)
198
232
  this.#refuse(names.uploadFieldOnModel, "it takes a browser `FileList`, which an agent has no way to hold.");
@@ -25,7 +25,8 @@ export class StoreSurfaceSource implements SurfaceSource {
25
25
 
26
26
  tools = (view: string[] = []): ToolEntry[] => {
27
27
  const viewKey = view.join(".");
28
- const bridge = (this.#bridge ??= AgentBridge.of());
28
+ this.#bridge ??= AgentBridge.of();
29
+ const bridge = this.#bridge;
29
30
  const live = bridge.toolsFor(viewKey);
30
31
  const cached = this.#wrapped.get(viewKey);
31
32
  if (cached?.source !== live)
@@ -107,7 +108,10 @@ export class StoreSurfaceSource implements SurfaceSource {
107
108
  additionalProperties: false,
108
109
  },
109
110
  effect: "state",
110
- run: (args: Record<string, unknown>) => (this.#bridge ??= AgentBridge.of()).read(String(args.key), viewKey),
111
+ run: (args: Record<string, unknown>) => {
112
+ this.#bridge ??= AgentBridge.of();
113
+ return this.#bridge.read(String(args.key), viewKey);
114
+ },
111
115
  };
112
116
  }
113
117
 
@@ -0,0 +1 @@
1
+ export * from "akanjs/fetch";