@tanstack-ai-browser/afm 0.0.0-stage → 0.2.1
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/LICENSE +21 -0
- package/Package.swift +33 -0
- package/README.md +62 -2
- package/dist/adapter.d.ts +96 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +490 -0
- package/dist/electron.d.ts +22 -0
- package/dist/electron.d.ts.map +1 -0
- package/dist/electron.js +131 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/messages.d.ts +12 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +29 -0
- package/dist/model-meta.d.ts +23 -0
- package/dist/model-meta.d.ts.map +1 -0
- package/dist/model-meta.js +20 -0
- package/dist/native.d.ts +163 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +21 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +133 -0
- package/electron/AfmNode.swift +81 -0
- package/electron/afm.c +94 -0
- package/electron/afm.node +0 -0
- package/electron/afm.node.sha256 +1 -0
- package/electron/build.mjs +54 -0
- package/electron/preload.cjs +35 -0
- package/electron/sources-hash.mjs +40 -0
- package/ios/Sources/AfmCore/Bridge.swift +315 -0
- package/ios/Sources/AfmCore/Conversation.swift +49 -0
- package/ios/Sources/AfmCore/Schema.swift +90 -0
- package/ios/Sources/AfmPlugin/AfmPlugin.swift +89 -0
- package/package.json +98 -4
- package/src/adapter.test.ts +380 -0
- package/src/adapter.ts +574 -0
- package/src/electron.ts +180 -0
- package/src/index.ts +27 -0
- package/src/messages.ts +37 -0
- package/src/model-meta.ts +37 -0
- package/src/native-build.test.ts +13 -0
- package/src/native.ts +119 -0
- package/src/schema.test.ts +146 -0
- package/src/schema.ts +153 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStack AI's messages as a native session's conversation (`NativeMessage`):
|
|
3
|
+
* text, the assistant's tool calls, and the tools' results. The framework
|
|
4
|
+
* reads text only here, so other parts are left out.
|
|
5
|
+
*/
|
|
6
|
+
import type { ModelMessage } from "@tanstack/ai";
|
|
7
|
+
import type { NativeMessage } from "./native.js";
|
|
8
|
+
/** A message's text: its string, or its text parts joined. */
|
|
9
|
+
export declare function contentText(content: ModelMessage["content"]): string;
|
|
10
|
+
/** The messages as a session's conversation, each tool result named by its call's tool. */
|
|
11
|
+
export declare function toNativeMessages(messages: ReadonlyArray<ModelMessage>): Array<NativeMessage>;
|
|
12
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD,8DAA8D;AAC9D,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,GAAG,MAAM,CAOpE;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,CAgB5F"}
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A message's text: its string, or its text parts joined. */
|
|
2
|
+
export function contentText(content) {
|
|
3
|
+
if (typeof content === "string")
|
|
4
|
+
return content;
|
|
5
|
+
if (!Array.isArray(content))
|
|
6
|
+
return "";
|
|
7
|
+
return content
|
|
8
|
+
.map((part) => (part.type === "text" ? part.content : ""))
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
.join("\n");
|
|
11
|
+
}
|
|
12
|
+
/** The messages as a session's conversation, each tool result named by its call's tool. */
|
|
13
|
+
export function toNativeMessages(messages) {
|
|
14
|
+
const toolNames = new Map();
|
|
15
|
+
return messages.map((m) => {
|
|
16
|
+
if (m.role === "tool") {
|
|
17
|
+
const id = m.toolCallId ?? "";
|
|
18
|
+
return { role: "tool", toolCallId: id, name: m.name ?? toolNames.get(id) ?? "", text: contentText(m.content) };
|
|
19
|
+
}
|
|
20
|
+
if (m.role === "assistant") {
|
|
21
|
+
const toolCalls = (m.toolCalls ?? []).map((call) => {
|
|
22
|
+
toolNames.set(call.id, call.function.name);
|
|
23
|
+
return { id: call.id, name: call.function.name, arguments: call.function.arguments || "{}" };
|
|
24
|
+
});
|
|
25
|
+
return { role: "assistant", text: contentText(m.content), ...(toolCalls.length > 0 && { toolCalls }) };
|
|
26
|
+
}
|
|
27
|
+
return { role: "user", text: contentText(m.content) };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model the framework runs: Apple's system language model
|
|
3
|
+
* (`SystemLanguageModel.default`), the on-device model of Apple Intelligence.
|
|
4
|
+
* It calls tools natively, and holds structured answers to a schema. It doesn't
|
|
5
|
+
* reason, so `chat()` offers no `reasoning` option for it.
|
|
6
|
+
*/
|
|
7
|
+
/** What a Foundation Models model supports. */
|
|
8
|
+
export type AfmModelMeta = {
|
|
9
|
+
/** The model's name here. */
|
|
10
|
+
name: string;
|
|
11
|
+
supports: {
|
|
12
|
+
input: ReadonlyArray<"text">;
|
|
13
|
+
output: ReadonlyArray<"text">;
|
|
14
|
+
capabilities: ReadonlyArray<"tools" | "structured-output">;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/** The models the framework runs: Apple's system language model. */
|
|
18
|
+
export declare const AFM_CHAT_MODELS: readonly ["system"];
|
|
19
|
+
/** A model the framework runs. */
|
|
20
|
+
export type AfmChatModel = (typeof AFM_CHAT_MODELS)[number];
|
|
21
|
+
/** Each model's metadata, by name. */
|
|
22
|
+
export declare const AFM_MODEL_META: Readonly<Record<AfmChatModel, AfmModelMeta>>;
|
|
23
|
+
//# sourceMappingURL=model-meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-meta.d.ts","sourceRoot":"","sources":["../src/model-meta.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+CAA+C;AAC/C,MAAM,MAAM,YAAY,GAAG;IACzB,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE;QACR,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;QAC5B,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;QAC7B,YAAY,EAAE,aAAa,CAAC,OAAO,GAAG,mBAAmB,CAAC,CAAA;KAC3D,CAAA;CACF,CAAA;AAWD,oEAAoE;AACpE,eAAO,MAAM,eAAe,qBAA+B,CAAA;AAE3D,kCAAkC;AAClC,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAE3D,sCAAsC;AACtC,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,CAEvE,CAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model the framework runs: Apple's system language model
|
|
3
|
+
* (`SystemLanguageModel.default`), the on-device model of Apple Intelligence.
|
|
4
|
+
* It calls tools natively, and holds structured answers to a schema. It doesn't
|
|
5
|
+
* reason, so `chat()` offers no `reasoning` option for it.
|
|
6
|
+
*/
|
|
7
|
+
const SYSTEM_MODEL = {
|
|
8
|
+
name: "system",
|
|
9
|
+
supports: {
|
|
10
|
+
input: ["text"],
|
|
11
|
+
output: ["text"],
|
|
12
|
+
capabilities: ["tools", "structured-output"]
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** The models the framework runs: Apple's system language model. */
|
|
16
|
+
export const AFM_CHAT_MODELS = [SYSTEM_MODEL.name];
|
|
17
|
+
/** Each model's metadata, by name. */
|
|
18
|
+
export const AFM_MODEL_META = {
|
|
19
|
+
[SYSTEM_MODEL.name]: SYSTEM_MODEL
|
|
20
|
+
};
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native side of the adapter: Apple's Foundation Models framework, reached
|
|
3
|
+
* through the Capacitor plugin on iOS (ios/Sources), or the Electron main
|
|
4
|
+
* process (`serveAfm()`, whose preload puts it at `window.afm`). Any host can
|
|
5
|
+
* stand in for them by implementing `AfmNative`.
|
|
6
|
+
*/
|
|
7
|
+
import type { PluginListenerHandle } from "@capacitor/core";
|
|
8
|
+
/** Why the model can't run: Apple's reasons, or an OS before 26. */
|
|
9
|
+
export type UnavailableReason = "deviceNotEligible" | "appleIntelligenceNotEnabled" | "modelNotReady" | "unsupportedOS" | "unavailable";
|
|
10
|
+
export type NativeAvailability = {
|
|
11
|
+
available: boolean;
|
|
12
|
+
reason?: UnavailableReason;
|
|
13
|
+
/** Tokens a session holds (iOS/macOS 26.4 and later). */
|
|
14
|
+
contextSize?: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A schema as the framework takes it: JSON Schema read into an ordered form
|
|
18
|
+
* (`schema.ts`), properties in the order given, and named definitions for
|
|
19
|
+
* `reference`s.
|
|
20
|
+
*/
|
|
21
|
+
export type NativeSchema = {
|
|
22
|
+
kind: "object";
|
|
23
|
+
name: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
properties: Array<NativeProperty>;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "array";
|
|
28
|
+
items: NativeSchema;
|
|
29
|
+
minimumElements?: number;
|
|
30
|
+
maximumElements?: number;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "string";
|
|
33
|
+
choices?: Array<string>;
|
|
34
|
+
constant?: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "integer" | "number";
|
|
37
|
+
minimum?: number;
|
|
38
|
+
maximum?: number;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "boolean";
|
|
41
|
+
} | {
|
|
42
|
+
kind: "null";
|
|
43
|
+
} | {
|
|
44
|
+
kind: "anyOf";
|
|
45
|
+
name: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
choices: Array<NativeSchema>;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "reference";
|
|
50
|
+
name: string;
|
|
51
|
+
};
|
|
52
|
+
export type NativeProperty = {
|
|
53
|
+
name: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
optional: boolean;
|
|
56
|
+
schema: NativeSchema;
|
|
57
|
+
};
|
|
58
|
+
/** A root schema and the definitions its references name. */
|
|
59
|
+
export type NativeGenerationSchema = {
|
|
60
|
+
root: NativeSchema;
|
|
61
|
+
definitions: Array<NativeSchema>;
|
|
62
|
+
};
|
|
63
|
+
export type NativeTool = {
|
|
64
|
+
name: string;
|
|
65
|
+
description: string;
|
|
66
|
+
parameters: NativeGenerationSchema;
|
|
67
|
+
};
|
|
68
|
+
/** A message of the conversation a session starts from. */
|
|
69
|
+
export type NativeMessage = {
|
|
70
|
+
role: "user";
|
|
71
|
+
text: string;
|
|
72
|
+
} | {
|
|
73
|
+
role: "assistant";
|
|
74
|
+
text: string;
|
|
75
|
+
toolCalls?: Array<{
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
arguments: string;
|
|
79
|
+
}>;
|
|
80
|
+
} | {
|
|
81
|
+
role: "tool";
|
|
82
|
+
toolCallId: string;
|
|
83
|
+
name: string;
|
|
84
|
+
text: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* A response's tokens, over every pass of the model (a tool call's too).
|
|
88
|
+
* `cachedTokens` is there only where the framework counts it right: for a
|
|
89
|
+
* session without tools.
|
|
90
|
+
*/
|
|
91
|
+
export type NativeUsage = {
|
|
92
|
+
inputTokens: number;
|
|
93
|
+
cachedTokens?: number;
|
|
94
|
+
outputTokens: number;
|
|
95
|
+
};
|
|
96
|
+
/** What a session sends while it responds, for the `turn` of the `respond` call. */
|
|
97
|
+
export type NativeEvent = {
|
|
98
|
+
sessionId: string;
|
|
99
|
+
turn: number;
|
|
100
|
+
} & ({
|
|
101
|
+
type: "delta";
|
|
102
|
+
text: string;
|
|
103
|
+
} | {
|
|
104
|
+
type: "toolCalls";
|
|
105
|
+
calls: Array<{
|
|
106
|
+
id: string;
|
|
107
|
+
name: string;
|
|
108
|
+
arguments: string;
|
|
109
|
+
}>;
|
|
110
|
+
} | {
|
|
111
|
+
type: "done";
|
|
112
|
+
text: string;
|
|
113
|
+
usage?: NativeUsage;
|
|
114
|
+
} | {
|
|
115
|
+
type: "error";
|
|
116
|
+
code: string;
|
|
117
|
+
message: string;
|
|
118
|
+
});
|
|
119
|
+
export interface AfmNative {
|
|
120
|
+
availability(): Promise<NativeAvailability>;
|
|
121
|
+
createSession(options: {
|
|
122
|
+
sessionId: string;
|
|
123
|
+
instructions?: string;
|
|
124
|
+
tools: Array<NativeTool>;
|
|
125
|
+
messages: Array<NativeMessage>;
|
|
126
|
+
}): Promise<void>;
|
|
127
|
+
prewarm(options: {
|
|
128
|
+
sessionId: string;
|
|
129
|
+
}): Promise<void>;
|
|
130
|
+
/** Prompts a session; the response comes as events. A `schema` holds the answer to it (JSON in `done`). */
|
|
131
|
+
respond(options: {
|
|
132
|
+
sessionId: string;
|
|
133
|
+
turn: number;
|
|
134
|
+
prompt: string;
|
|
135
|
+
options?: {
|
|
136
|
+
temperature?: number;
|
|
137
|
+
maximumResponseTokens?: number;
|
|
138
|
+
/** Whether the model may call the session's tools, must, or mustn't (iOS/macOS 27). Left out, the framework decides. */
|
|
139
|
+
toolCallingMode?: "allowed" | "required" | "disallowed";
|
|
140
|
+
};
|
|
141
|
+
schema?: NativeGenerationSchema;
|
|
142
|
+
}): Promise<void>;
|
|
143
|
+
/** A tool's result: the session's waiting call returns it, and the response goes on. */
|
|
144
|
+
submitToolResult(options: {
|
|
145
|
+
sessionId: string;
|
|
146
|
+
callId: string;
|
|
147
|
+
output: string;
|
|
148
|
+
}): Promise<void>;
|
|
149
|
+
/** Stops a session's response. */
|
|
150
|
+
cancel(options: {
|
|
151
|
+
sessionId: string;
|
|
152
|
+
}): Promise<void>;
|
|
153
|
+
deleteSession(options: {
|
|
154
|
+
sessionId: string;
|
|
155
|
+
}): Promise<void>;
|
|
156
|
+
addListener(eventName: "event", listener: (event: NativeEvent) => void): Promise<PluginListenerHandle>;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The Capacitor plugin (iOS), loading Capacitor on its first call. Elsewhere
|
|
160
|
+
* its calls reject: Capacitor has no web version of it.
|
|
161
|
+
*/
|
|
162
|
+
export declare const Afm: AfmNative;
|
|
163
|
+
//# sourceMappingURL=native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"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"}
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
let loading;
|
|
2
|
+
/**
|
|
3
|
+
* The plugin, loading Capacitor on first use: pages elsewhere never load it.
|
|
4
|
+
* It's wrapped: a Capacitor plugin answers every property, `then` too, so a
|
|
5
|
+
* promise of the plugin itself would wait on it forever.
|
|
6
|
+
*/
|
|
7
|
+
const capacitor = () => (loading ??= import("@capacitor/core").then(({ registerPlugin }) => ({ plugin: registerPlugin("Afm") })));
|
|
8
|
+
/**
|
|
9
|
+
* The Capacitor plugin (iOS), loading Capacitor on its first call. Elsewhere
|
|
10
|
+
* its calls reject: Capacitor has no web version of it.
|
|
11
|
+
*/
|
|
12
|
+
export const Afm = {
|
|
13
|
+
availability: async () => (await capacitor()).plugin.availability(),
|
|
14
|
+
createSession: async (options) => (await capacitor()).plugin.createSession(options),
|
|
15
|
+
prewarm: async (options) => (await capacitor()).plugin.prewarm(options),
|
|
16
|
+
respond: async (options) => (await capacitor()).plugin.respond(options),
|
|
17
|
+
submitToolResult: async (options) => (await capacitor()).plugin.submitToolResult(options),
|
|
18
|
+
cancel: async (options) => (await capacitor()).plugin.cancel(options),
|
|
19
|
+
deleteSession: async (options) => (await capacitor()).plugin.deleteSession(options),
|
|
20
|
+
addListener: async (name, listener) => (await capacitor()).plugin.addListener(name, listener)
|
|
21
|
+
};
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema (what `chat()` gives adapters for tools and structured output)
|
|
3
|
+
* read into the framework's own schema (`NativeSchema`), which the model's
|
|
4
|
+
* output is held to: a tool's arguments always fit its parameters.
|
|
5
|
+
*
|
|
6
|
+
* Properties keep their order, which the model writes them in. Each object is
|
|
7
|
+
* named by its path, so two objects never share a name. A property that may be
|
|
8
|
+
* null is optional. `$ref`s to `$defs` (or `definitions`) become references to
|
|
9
|
+
* named definitions; one to a plain value is written in place.
|
|
10
|
+
*/
|
|
11
|
+
import type { NativeGenerationSchema } from "./native.js";
|
|
12
|
+
/**
|
|
13
|
+
* The framework's schema for a JSON Schema, named `name` (a tool's name, or
|
|
14
|
+
* the structured answer's).
|
|
15
|
+
*/
|
|
16
|
+
export declare function toNativeSchema(schema: unknown, name: string): NativeGenerationSchema;
|
|
17
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAgB,MAAM,aAAa,CAAA;AA2BvE;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,sBAAsB,CA+GpF"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2
|
+
/** Drops undefined fields: the native bridge takes JSON. */
|
|
3
|
+
function defined(value) {
|
|
4
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
5
|
+
}
|
|
6
|
+
const isNull = (schema) => isObject(schema) && (schema.type === "null" || (Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null"));
|
|
7
|
+
/** Whether a schema allows null: `type: [..., "null"]`, or a null choice in `anyOf`. */
|
|
8
|
+
function nullable(schema) {
|
|
9
|
+
if (!isObject(schema))
|
|
10
|
+
return false;
|
|
11
|
+
if (Array.isArray(schema.type) && schema.type.includes("null"))
|
|
12
|
+
return true;
|
|
13
|
+
const variants = schema.anyOf ?? schema.oneOf;
|
|
14
|
+
return Array.isArray(variants) && variants.some(isNull);
|
|
15
|
+
}
|
|
16
|
+
const number = (value) => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
|
|
17
|
+
/** A schema's name for the framework: letters, digits and underscores. */
|
|
18
|
+
const typeName = (name) => name.replace(/[^A-Za-z0-9_]/g, "_") || "Value";
|
|
19
|
+
/**
|
|
20
|
+
* The framework's schema for a JSON Schema, named `name` (a tool's name, or
|
|
21
|
+
* the structured answer's).
|
|
22
|
+
*/
|
|
23
|
+
export function toNativeSchema(schema, name) {
|
|
24
|
+
const json = isObject(schema) ? schema : {};
|
|
25
|
+
const defs = { ...(isObject(json.definitions) ? json.definitions : {}), ...(isObject(json.$defs) ? json.$defs : {}) };
|
|
26
|
+
const definitions = new Map();
|
|
27
|
+
const convert = (value, path) => {
|
|
28
|
+
if (!isObject(value))
|
|
29
|
+
return { kind: "string" };
|
|
30
|
+
if (typeof value.$ref === "string") {
|
|
31
|
+
const target = value.$ref.split("/").at(-1) ?? "";
|
|
32
|
+
const def = defs[target];
|
|
33
|
+
if (!isObject(def))
|
|
34
|
+
return { kind: "string" };
|
|
35
|
+
const converted = definitions.get(target) ?? convertDefinition(def, target);
|
|
36
|
+
// Only named schemas (objects, choices) can be referred to.
|
|
37
|
+
return converted.kind === "object" || converted.kind === "anyOf" ? { kind: "reference", name: typeName(target) } : converted;
|
|
38
|
+
}
|
|
39
|
+
if ("const" in value && typeof value.const === "string")
|
|
40
|
+
return { kind: "string", constant: value.const };
|
|
41
|
+
if (Array.isArray(value.enum)) {
|
|
42
|
+
const members = value.enum.filter((v) => v !== null);
|
|
43
|
+
if (members.every((v) => typeof v === "string")) {
|
|
44
|
+
return members.length > 0 ? { kind: "string", choices: members } : { kind: "string" };
|
|
45
|
+
}
|
|
46
|
+
// true and false both: any boolean. One alone, the framework can't hold it to.
|
|
47
|
+
if (members.every((v) => typeof v === "boolean")) {
|
|
48
|
+
if (new Set(members).size === 2)
|
|
49
|
+
return { kind: "boolean" };
|
|
50
|
+
throw new Error(`${path}: an enum of only ${String(members[0])} isn't supported (Apple's framework can't hold a boolean to one value)`);
|
|
51
|
+
}
|
|
52
|
+
// Integers (and strings among them): a choice of exact values, each integer
|
|
53
|
+
// a range of itself, which the framework holds the model to. A decimal
|
|
54
|
+
// can't be: a range of one decimal fails in Apple's generator.
|
|
55
|
+
if (members.some((v) => typeof v === "number" && !Number.isInteger(v))) {
|
|
56
|
+
throw new Error(`${path}: an enum of decimal numbers isn't supported (Apple's framework can't hold a decimal to exact values)`);
|
|
57
|
+
}
|
|
58
|
+
if (members.every((v) => typeof v === "string" || Number.isInteger(v))) {
|
|
59
|
+
const choices = members.map((v) => typeof v === "string" ? { kind: "string", constant: v } : { kind: "integer", minimum: v, maximum: v });
|
|
60
|
+
if (choices.length === 1)
|
|
61
|
+
return choices[0];
|
|
62
|
+
return defined({ kind: "anyOf", name: typeName(path), description: typeof value.description === "string" ? value.description : undefined, choices });
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`${path}: an enum mixing ${[...new Set(members.map((v) => typeof v))].join(" and ")} values isn't supported by Apple's framework`);
|
|
65
|
+
}
|
|
66
|
+
const variants = value.anyOf ?? value.oneOf;
|
|
67
|
+
if (Array.isArray(variants)) {
|
|
68
|
+
const choices = variants.filter((v) => !isNull(v));
|
|
69
|
+
if (choices.length === 1)
|
|
70
|
+
return convert({ ...choices[0], description: value.description ?? choices[0].description }, path);
|
|
71
|
+
if (choices.length > 1) {
|
|
72
|
+
return defined({
|
|
73
|
+
kind: "anyOf",
|
|
74
|
+
name: typeName(path),
|
|
75
|
+
description: typeof value.description === "string" ? value.description : undefined,
|
|
76
|
+
choices: choices.map((choice, i) => convert(choice, `${path}_${i + 1}`))
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const types = Array.isArray(value.type) ? value.type.filter((t) => t !== "null") : [value.type];
|
|
81
|
+
const type = typeof types[0] === "string" ? types[0] : isObject(value.properties) ? "object" : value.items ? "array" : "string";
|
|
82
|
+
switch (type) {
|
|
83
|
+
case "object": {
|
|
84
|
+
const required = Array.isArray(value.required) ? value.required : [];
|
|
85
|
+
const properties = Object.entries(isObject(value.properties) ? value.properties : {}).map(([key, property]) => defined({
|
|
86
|
+
name: key,
|
|
87
|
+
description: isObject(property) && typeof property.description === "string" ? property.description : undefined,
|
|
88
|
+
optional: !required.includes(key) || nullable(property),
|
|
89
|
+
schema: convert(property, `${path}_${key}`)
|
|
90
|
+
}));
|
|
91
|
+
return defined({
|
|
92
|
+
kind: "object",
|
|
93
|
+
name: typeName(path),
|
|
94
|
+
description: typeof value.description === "string" ? value.description : undefined,
|
|
95
|
+
properties
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
case "array":
|
|
99
|
+
return defined({
|
|
100
|
+
kind: "array",
|
|
101
|
+
items: convert(Array.isArray(value.items) ? value.items[0] : value.items, `${path}_item`),
|
|
102
|
+
minimumElements: number(value.minItems),
|
|
103
|
+
maximumElements: number(value.maxItems)
|
|
104
|
+
});
|
|
105
|
+
case "integer":
|
|
106
|
+
case "number":
|
|
107
|
+
return defined({
|
|
108
|
+
kind: type,
|
|
109
|
+
minimum: number(value.minimum) ?? number(value.exclusiveMinimum),
|
|
110
|
+
maximum: number(value.maximum) ?? number(value.exclusiveMaximum)
|
|
111
|
+
});
|
|
112
|
+
case "boolean":
|
|
113
|
+
return { kind: "boolean" };
|
|
114
|
+
case "null":
|
|
115
|
+
return { kind: "null" };
|
|
116
|
+
default:
|
|
117
|
+
return { kind: "string" };
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const convertDefinition = (def, target) => {
|
|
121
|
+
// Set first, so a definition that refers to itself finds its name.
|
|
122
|
+
definitions.set(target, { kind: "object", name: typeName(target), properties: [] });
|
|
123
|
+
const converted = convert(def, target);
|
|
124
|
+
const named = converted.kind === "object" || converted.kind === "anyOf" ? { ...converted, name: typeName(target) } : converted;
|
|
125
|
+
definitions.set(target, named);
|
|
126
|
+
return named;
|
|
127
|
+
};
|
|
128
|
+
const root = convert(json, name);
|
|
129
|
+
return {
|
|
130
|
+
root,
|
|
131
|
+
definitions: [...definitions.values()].filter((d) => d.kind === "object" || d.kind === "anyOf")
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// AfmBridge for the Node-API addon (afm.c) that serveAfm() loads: plain C functions,
|
|
2
|
+
// JSON strings in and out, built into one afm.node with AfmCore (build.mjs). macOS only.
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
/// The C function the addon gives for events: called with each event in JSON, from
|
|
6
|
+
/// any thread.
|
|
7
|
+
public typealias AfmListener = @convention(c) (UnsafePointer<CChar>) -> Void
|
|
8
|
+
|
|
9
|
+
private final class State: @unchecked Sendable {
|
|
10
|
+
let lock = NSLock()
|
|
11
|
+
var listener: AfmListener?
|
|
12
|
+
var bridge: AnyObject?
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private let state = State()
|
|
16
|
+
|
|
17
|
+
private func json(_ object: Any) -> String {
|
|
18
|
+
guard JSONSerialization.isValidJSONObject(object),
|
|
19
|
+
let data = try? JSONSerialization.data(withJSONObject: object) else { return "{}" }
|
|
20
|
+
return String(decoding: data, as: UTF8.self)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private func emit(_ event: [String: Any]) {
|
|
24
|
+
let listener = state.lock.withLock { state.listener }
|
|
25
|
+
guard let listener else { return }
|
|
26
|
+
json(event).withCString { listener($0) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@available(macOS 26.0, *)
|
|
30
|
+
private func bridge() -> AfmBridge {
|
|
31
|
+
state.lock.withLock {
|
|
32
|
+
if let bridge = state.bridge as? AfmBridge { return bridge }
|
|
33
|
+
let bridge = AfmBridge { emit($0) }
|
|
34
|
+
state.bridge = bridge
|
|
35
|
+
return bridge
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Sets the function events go to.
|
|
40
|
+
@_cdecl("afm_set_listener")
|
|
41
|
+
public func afm_set_listener(_ listener: AfmListener?) {
|
|
42
|
+
state.lock.withLock { state.listener = listener }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Calls one of AfmBridge's methods with its options in JSON. Returns
|
|
46
|
+
/// `{ "result": … }` or `{ "error": … }` in JSON, which the caller frees with `afm_free`.
|
|
47
|
+
@_cdecl("afm_call")
|
|
48
|
+
public func afm_call(_ method: UnsafePointer<CChar>, _ options: UnsafePointer<CChar>) -> UnsafeMutablePointer<CChar> {
|
|
49
|
+
let name = String(cString: method)
|
|
50
|
+
let object = (try? JSONSerialization.jsonObject(with: Data(String(cString: options).utf8))) as? [String: Any] ?? [:]
|
|
51
|
+
var reply: [String: Any]
|
|
52
|
+
if #available(macOS 26.0, *) {
|
|
53
|
+
do {
|
|
54
|
+
switch name {
|
|
55
|
+
case "availability": reply = ["result": AfmBridge.availability()]
|
|
56
|
+
case "createSession": try bridge().createSession(object); reply = ["result": NSNull()]
|
|
57
|
+
case "prewarm": try bridge().prewarm(object); reply = ["result": NSNull()]
|
|
58
|
+
case "respond": try bridge().respond(object); reply = ["result": NSNull()]
|
|
59
|
+
case "submitToolResult": try bridge().submitToolResult(object); reply = ["result": NSNull()]
|
|
60
|
+
case "cancel": try bridge().cancel(object); reply = ["result": NSNull()]
|
|
61
|
+
case "deleteSession":
|
|
62
|
+
bridge().deleteSession(object["sessionId"] as? String ?? "")
|
|
63
|
+
reply = ["result": NSNull()]
|
|
64
|
+
default: reply = ["error": "Unknown method \(name)"]
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
reply = ["error": String(describing: error)]
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
reply = name == "availability"
|
|
71
|
+
? ["result": ["available": false, "reason": "unsupportedOS"]]
|
|
72
|
+
: ["error": "Apple's on-device model needs macOS 26 or later"]
|
|
73
|
+
}
|
|
74
|
+
return strdup(json(reply))!
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Frees a string `afm_call` returned.
|
|
78
|
+
@_cdecl("afm_free")
|
|
79
|
+
public func afm_free(_ pointer: UnsafeMutablePointer<CChar>?) {
|
|
80
|
+
free(pointer)
|
|
81
|
+
}
|
package/electron/afm.c
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The Node-API addon around AfmBridge (AfmNode.swift): `call(method, optionsJson)`
|
|
2
|
+
// returns `{ result }` or `{ error }` in JSON, and `setListener(fn)` gets each
|
|
3
|
+
// event in JSON. Events come from Swift's threads, so they reach JavaScript through
|
|
4
|
+
// a thread-safe function, on Node's own thread.
|
|
5
|
+
#define NAPI_VERSION 8
|
|
6
|
+
#include <node_api.h>
|
|
7
|
+
#include <stdlib.h>
|
|
8
|
+
#include <string.h>
|
|
9
|
+
|
|
10
|
+
typedef void (*afm_listener)(const char *json);
|
|
11
|
+
extern void afm_set_listener(afm_listener listener);
|
|
12
|
+
extern char *afm_call(const char *method, const char *options);
|
|
13
|
+
extern void afm_free(char *pointer);
|
|
14
|
+
|
|
15
|
+
static napi_threadsafe_function events = NULL;
|
|
16
|
+
|
|
17
|
+
static void call_js(napi_env env, napi_value callback, void *context, void *data) {
|
|
18
|
+
(void)context;
|
|
19
|
+
char *json = data;
|
|
20
|
+
if (env != NULL && callback != NULL) {
|
|
21
|
+
napi_value argument, undefined;
|
|
22
|
+
napi_create_string_utf8(env, json, NAPI_AUTO_LENGTH, &argument);
|
|
23
|
+
napi_get_undefined(env, &undefined);
|
|
24
|
+
napi_call_function(env, undefined, callback, 1, &argument, NULL);
|
|
25
|
+
}
|
|
26
|
+
free(json);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static void on_event(const char *json) {
|
|
30
|
+
napi_threadsafe_function target = events;
|
|
31
|
+
if (target == NULL) return;
|
|
32
|
+
char *copy = strdup(json);
|
|
33
|
+
if (napi_call_threadsafe_function(target, copy, napi_tsfn_nonblocking) != napi_ok) free(copy);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static char *string_argument(napi_env env, napi_value value) {
|
|
37
|
+
size_t length = 0;
|
|
38
|
+
if (napi_get_value_string_utf8(env, value, NULL, 0, &length) != napi_ok) return NULL;
|
|
39
|
+
char *string = malloc(length + 1);
|
|
40
|
+
napi_get_value_string_utf8(env, value, string, length + 1, &length);
|
|
41
|
+
return string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static napi_value set_listener(napi_env env, napi_callback_info info) {
|
|
45
|
+
size_t count = 1;
|
|
46
|
+
napi_value arguments[1];
|
|
47
|
+
napi_get_cb_info(env, info, &count, arguments, NULL, NULL);
|
|
48
|
+
if (events != NULL) {
|
|
49
|
+
afm_set_listener(NULL);
|
|
50
|
+
napi_release_threadsafe_function(events, napi_tsfn_abort);
|
|
51
|
+
events = NULL;
|
|
52
|
+
}
|
|
53
|
+
napi_valuetype type = napi_undefined;
|
|
54
|
+
if (count > 0) napi_typeof(env, arguments[0], &type);
|
|
55
|
+
if (type == napi_function) {
|
|
56
|
+
napi_value name;
|
|
57
|
+
napi_create_string_utf8(env, "afm events", NAPI_AUTO_LENGTH, &name);
|
|
58
|
+
napi_create_threadsafe_function(env, arguments[0], NULL, name, 0, 1, NULL, NULL, NULL, call_js, &events);
|
|
59
|
+
// Events don't keep the process running.
|
|
60
|
+
napi_unref_threadsafe_function(env, events);
|
|
61
|
+
afm_set_listener(on_event);
|
|
62
|
+
}
|
|
63
|
+
return NULL;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static napi_value call(napi_env env, napi_callback_info info) {
|
|
67
|
+
size_t count = 2;
|
|
68
|
+
napi_value arguments[2];
|
|
69
|
+
napi_get_cb_info(env, info, &count, arguments, NULL, NULL);
|
|
70
|
+
char *method = count > 0 ? string_argument(env, arguments[0]) : NULL;
|
|
71
|
+
char *options = count > 1 ? string_argument(env, arguments[1]) : NULL;
|
|
72
|
+
if (method == NULL || options == NULL) {
|
|
73
|
+
free(method);
|
|
74
|
+
free(options);
|
|
75
|
+
napi_throw_type_error(env, NULL, "call(method, optionsJson) takes two strings");
|
|
76
|
+
return NULL;
|
|
77
|
+
}
|
|
78
|
+
char *reply = afm_call(method, options);
|
|
79
|
+
napi_value result;
|
|
80
|
+
napi_create_string_utf8(env, reply, NAPI_AUTO_LENGTH, &result);
|
|
81
|
+
afm_free(reply);
|
|
82
|
+
free(method);
|
|
83
|
+
free(options);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
NAPI_MODULE_INIT() {
|
|
88
|
+
napi_property_descriptor properties[] = {
|
|
89
|
+
{ "call", NULL, call, NULL, NULL, NULL, napi_default, NULL },
|
|
90
|
+
{ "setListener", NULL, set_listener, NULL, NULL, NULL, napi_default, NULL }
|
|
91
|
+
};
|
|
92
|
+
napi_define_properties(env, exports, 2, properties);
|
|
93
|
+
return exports;
|
|
94
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2e804dcb3921aad737e0924db1975955882393feff18f01dddd48bb5a7855ad3
|