akanjs 3.0.0-alpha.17 → 3.0.0-alpha.18
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/labelOf.ts +2 -2
- package/fetch/client/fetchClient.ts +34 -12
- 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/rscWorker.tsx +6 -2
- package/server/vendor/akanjs-fetch.ts +1 -0
- package/service/predefinedAdaptor/deepseekLlm.ts +1 -1
- package/store/agent/ScreenReader.ts +28 -9
- package/store/agent/StoreCatalogue.ts +39 -5
- package/store/agent/StoreSurfaceSource.ts +6 -2
- package/types/server/vendor/akanjs-fetch.d.ts +1 -0
package/constant/labelOf.ts
CHANGED
|
@@ -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?:
|
|
10
|
-
const titlePath = paths
|
|
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
|
|
61
|
-
|
|
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 { ...
|
|
111
|
+
return { ...sharedSignalRegistry.signal };
|
|
97
112
|
}
|
|
98
113
|
static resetSharedRegistry() {
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
151
|
-
|
|
152
|
-
this.#sharedRegistryAppliedVersion =
|
|
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 ===
|
|
187
|
-
this.applySignal(
|
|
188
|
-
this.#sharedRegistryAppliedVersion =
|
|
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(
|
|
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 },
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/rscWorker.tsx
CHANGED
|
@@ -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
|
-
|
|
1186
|
-
|
|
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-
|
|
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")
|
|
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")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
115
|
+
this.#flush(`${"#".repeat(level)} `);
|
|
116
|
+
return;
|
|
101
117
|
}
|
|
102
118
|
if (tag === "LI") {
|
|
103
119
|
this.#flush();
|
|
104
120
|
this.#walkChildren(el);
|
|
105
|
-
|
|
121
|
+
this.#flush("- ");
|
|
122
|
+
return;
|
|
106
123
|
}
|
|
107
124
|
if (tag === "TD" || tag === "TH") {
|
|
108
125
|
if (this.#buffer.trim()) this.#buffer += " |";
|
|
109
|
-
|
|
126
|
+
this.#walkChildren(el);
|
|
127
|
+
return;
|
|
110
128
|
}
|
|
111
129
|
if (blockTags.has(tag)) {
|
|
112
130
|
this.#flush();
|
|
113
131
|
this.#walkChildren(el);
|
|
114
|
-
|
|
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)
|
|
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
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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
|
-
|
|
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>) =>
|
|
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";
|