@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/Package.swift +33 -0
  3. package/README.md +62 -2
  4. package/dist/adapter.d.ts +96 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +490 -0
  7. package/dist/electron.d.ts +22 -0
  8. package/dist/electron.d.ts.map +1 -0
  9. package/dist/electron.js +131 -0
  10. package/dist/index.d.ts +17 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +12 -0
  13. package/dist/messages.d.ts +12 -0
  14. package/dist/messages.d.ts.map +1 -0
  15. package/dist/messages.js +29 -0
  16. package/dist/model-meta.d.ts +23 -0
  17. package/dist/model-meta.d.ts.map +1 -0
  18. package/dist/model-meta.js +20 -0
  19. package/dist/native.d.ts +163 -0
  20. package/dist/native.d.ts.map +1 -0
  21. package/dist/native.js +21 -0
  22. package/dist/schema.d.ts +17 -0
  23. package/dist/schema.d.ts.map +1 -0
  24. package/dist/schema.js +133 -0
  25. package/electron/AfmNode.swift +81 -0
  26. package/electron/afm.c +94 -0
  27. package/electron/afm.node +0 -0
  28. package/electron/afm.node.sha256 +1 -0
  29. package/electron/build.mjs +54 -0
  30. package/electron/preload.cjs +35 -0
  31. package/electron/sources-hash.mjs +40 -0
  32. package/ios/Sources/AfmCore/Bridge.swift +315 -0
  33. package/ios/Sources/AfmCore/Conversation.swift +49 -0
  34. package/ios/Sources/AfmCore/Schema.swift +90 -0
  35. package/ios/Sources/AfmPlugin/AfmPlugin.swift +89 -0
  36. package/package.json +98 -4
  37. package/src/adapter.test.ts +380 -0
  38. package/src/adapter.ts +574 -0
  39. package/src/electron.ts +180 -0
  40. package/src/index.ts +27 -0
  41. package/src/messages.ts +37 -0
  42. package/src/model-meta.ts +37 -0
  43. package/src/native-build.test.ts +13 -0
  44. package/src/native.ts +119 -0
  45. package/src/schema.test.ts +146 -0
  46. package/src/schema.ts +153 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 daveycodez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Package.swift ADDED
@@ -0,0 +1,33 @@
1
+ // swift-tools-version: 5.9
2
+
3
+ import PackageDescription
4
+
5
+ // AfmCore is the Swift side of the adapter (AfmBridge), with no Capacitor in it, so
6
+ // other hosts (an Electron addon, Tauri) build it too. AfmPlugin wraps it for
7
+ // Capacitor; `cap sync` adds this package to the iOS app.
8
+ let package = Package(
9
+ name: "TanstackAiBrowserAfm",
10
+ platforms: [.iOS(.v15), .macOS(.v14)],
11
+ products: [
12
+ .library(name: "TanstackAiBrowserAfm", targets: ["AfmPlugin"]),
13
+ .library(name: "AfmCore", targets: ["AfmCore"])
14
+ ],
15
+ dependencies: [
16
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
17
+ ],
18
+ targets: [
19
+ .target(
20
+ name: "AfmCore",
21
+ path: "ios/Sources/AfmCore"
22
+ ),
23
+ .target(
24
+ name: "AfmPlugin",
25
+ dependencies: [
26
+ "AfmCore",
27
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
28
+ .product(name: "Cordova", package: "capacitor-swift-pm")
29
+ ],
30
+ path: "ios/Sources/AfmPlugin"
31
+ )
32
+ ]
33
+ )
package/README.md CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # @tanstack-ai-browser/afm
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A [TanStack AI](https://tanstack.com/ai) adapter for Apple's on-device Foundation Model (AFM), the model behind Apple Intelligence, through Apple's Foundation Models framework itself. It ships as a Capacitor plugin for iOS and as `serveAfm()` for Electron on macOS; any other host (Tauri, say) can stand in.
4
+
5
+ Tool calling is the framework's own. The model's arguments always fit each tool's schema, and it can call several tools at once. A session with tools gets a sentence in its instructions on when to call them: without one, Apple's model on an iPhone calls a tool for every prompt, greetings included. `toolChoice: "required"` (or a named tool) makes the model call one. When it calls a tool, the native response waits; `chat()` runs the tool (approvals included) and the adapter hands the result back to the same response, without the model reading the conversation again.
6
+
7
+ ## Requirements
8
+
9
+ - iOS 26 or later on a device with Apple Intelligence (iPhone 15 Pro and later), with Apple Intelligence turned on. The iOS Simulator uses the Mac's model.
10
+ - Capacitor 8 with Swift Package Manager.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install @tanstack-ai-browser/afm @tanstack/ai @capacitor/core
16
+ npx cap sync ios
17
+ ```
18
+
19
+ ## Use
20
+
21
+ ```ts
22
+ import { chat } from "@tanstack/ai"
23
+ import { afmText } from "@tanstack-ai-browser/afm"
24
+
25
+ const adapter = afmText()
26
+ if ((await adapter.availability()) === "available") {
27
+ const stream = chat({ adapter, messages, tools })
28
+ }
29
+ ```
30
+
31
+ `availability()` is `"available"`, `"downloading"` while Apple Intelligence gets its model, or `"unavailable"` (an older OS, an ineligible device, or Apple Intelligence turned off). `contextSize()` is the tokens a session holds, on iOS 26.4 and later.
32
+
33
+ Each thread (`threadId`) keeps a native session, so a follow-up message only sends that message. Structured output (`chat({ outputSchema })`) is held to the schema by the framework.
34
+
35
+ ### Electron (macOS)
36
+
37
+ The page is the same as on iOS: `afmText()`. The main process runs the model, with one call before opening windows:
38
+
39
+ ```ts
40
+ // electron/main.js
41
+ import { app, BrowserWindow } from "electron"
42
+ import { serveAfm } from "@tanstack-ai-browser/afm"
43
+
44
+ serveAfm()
45
+ app.whenReady().then(() => new BrowserWindow().loadFile("index.html"))
46
+ ```
47
+
48
+ `serveAfm()` loads the package's Node-API addon (`electron/afm.node`, built from the same Swift as the Capacitor plugin) and adds the package's preload to the app's pages, which puts the model at `window.afm`, where `afmText()` finds it. It runs in the main process, the app's own with its window, so the framework treats it as a foreground app. It needs macOS 26 on Apple silicon; elsewhere the model reports itself unavailable.
49
+
50
+ `serveAfm({ directory, allow })`:
51
+
52
+ - `directory`: the folder with `afm.node` and `preload.cjs`, by default the package's `electron` folder, found from the app's `node_modules`. An app that leaves `node_modules` out of its package copies the two files in and passes their folder. Keep `afm.node` out of the asar archive (electron-builder unpacks `.node` files; otherwise list it in `asarUnpack`).
53
+ - `allow(url)`: which pages may use the model. By default the app's own: any page but an http(s) one on a host other than localhost.
54
+
55
+ Each frame's sessions are its own: a frame's calls reach only the sessions it created, their events go to that frame only (while `allow()` still allows its page), and they end when the frame navigates or closes.
56
+
57
+ `electron/afm.node` is committed, so the package builds and publishes without Xcode. After changing the Swift or C it's built from, rebuild it on a Mac with Xcode (`node electron/build.mjs`) and commit it with `electron/afm.node.sha256`: a test fails while the committed addon is older than its sources.
58
+
59
+ ### Another host
60
+
61
+ Pass any implementation of `AfmNative` (the plugin's methods and its `event` listener) as `native`, or put it at `window.afm`.
62
+
63
+ The Swift side is in `ios/Sources`: `AfmCore` (the bridge, with no Capacitor in it) and `AfmPlugin` (the Capacitor plugin). Electron's addon is in `electron`: `AfmNode.swift` (C functions over the bridge) and `afm.c` (Node-API).
@@ -0,0 +1,96 @@
1
+ import type { AdapterYieldChunk, TextOptions } from "@tanstack/ai";
2
+ import { BaseTextAdapter } from "@tanstack/ai/adapters";
3
+ import type { StructuredOutputOptions, StructuredOutputResult } from "@tanstack/ai/adapters";
4
+ import type { AfmChatModel } from "./model-meta.js";
5
+ import type { AfmNative } from "./native.js";
6
+ export type AfmConfig = {
7
+ /**
8
+ * The native side. By default `window.afm` where a host put it there
9
+ * (Electron, with `serveAfm()` in the main process), else the Capacitor
10
+ * plugin (`Afm`).
11
+ */
12
+ native?: AfmNative;
13
+ };
14
+ /** Per-call `modelOptions`: the response's sampling and length. */
15
+ export type AfmTextModelOptions = {
16
+ temperature?: number;
17
+ /** Most tokens to generate. */
18
+ maximumResponseTokens?: number;
19
+ };
20
+ export type Availability = "unavailable" | "downloadable" | "downloading" | "available";
21
+ type MetadataByModality = {
22
+ text: unknown;
23
+ image: unknown;
24
+ audio: unknown;
25
+ video: unknown;
26
+ document: unknown;
27
+ };
28
+ export declare class AfmTextAdapter extends BaseTextAdapter<AfmChatModel, AfmTextModelOptions, readonly ["text"], MetadataByModality> {
29
+ readonly kind: "text";
30
+ readonly name: "afm";
31
+ readonly provider: "afm";
32
+ readonly api: "afm";
33
+ readonly inputModalities: readonly ["text"];
34
+ private readonly native;
35
+ private readonly threads;
36
+ private readonly queues;
37
+ /**
38
+ * Responses waiting for tool results in sessions no thread keeps (prompt
39
+ * cache retention "none"), by thread: the next step resumes them.
40
+ */
41
+ private readonly paused;
42
+ private listening;
43
+ constructor(config: AfmConfig, model: AfmChatModel);
44
+ /** Whether Apple's model can run here: "downloading" while Apple Intelligence gets it. */
45
+ availability(): Promise<Availability>;
46
+ /** Tokens a session holds, where the OS says (26.4 and later). */
47
+ contextSize(): Promise<number | undefined>;
48
+ /** Resolves to this adapter if the model can run here, else rejects. */
49
+ create(options?: {
50
+ signal?: AbortSignal;
51
+ }): Promise<this>;
52
+ /**
53
+ * Ends every session: the threads', responses waiting for tool results, and
54
+ * responses streaming now (structured output's, and those of sessions no
55
+ * thread keeps), each once.
56
+ */
57
+ destroy(): Promise<void>;
58
+ /** Routes the native events to their thread's response. Listens once. */
59
+ private listen;
60
+ /** Ends a session. A response still streaming from it ends with an error. */
61
+ private close;
62
+ /** Keeps a thread's session, ending the least recently used past the limit. */
63
+ private keep;
64
+ /** A new native session for a thread, holding `messages`. */
65
+ private open;
66
+ /**
67
+ * The step's thread, and what it sends: the results of the tool calls its
68
+ * response waits on, a new prompt to its session, or (for a conversation the
69
+ * session doesn't hold) a new session and the last message as its prompt.
70
+ */
71
+ private begin;
72
+ chatStream(options: TextOptions<AfmTextModelOptions>): AsyncIterable<AdapterYieldChunk>;
73
+ /**
74
+ * A JSON answer for `chat({ outputSchema })`: a session holding the
75
+ * conversation answers its last message, held to the schema by the
76
+ * framework. `chat()` validates the result.
77
+ */
78
+ structuredOutput(options: StructuredOutputOptions<AfmTextModelOptions>): Promise<StructuredOutputResult<unknown>>;
79
+ }
80
+ /**
81
+ * A TanStack AI text adapter for Apple's on-device model, through the
82
+ * Foundation Models framework (the Capacitor plugin on iOS, by default).
83
+ * The model is Apple's system model ("system"), so naming it is optional.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * const adapter = afmText()
88
+ * if ((await adapter.availability()) === "available") {
89
+ * chat({ adapter, messages, tools })
90
+ * }
91
+ * ```
92
+ */
93
+ export declare function afmText(config?: AfmConfig): AfmTextAdapter;
94
+ export declare function afmText(model: AfmChatModel, config?: AfmConfig): AfmTextAdapter;
95
+ export {};
96
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,iBAAiB,EAAgB,WAAW,EAAoB,MAAM,cAAc,CAAA;AAClG,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAG5F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAEnD,OAAO,KAAK,EAAE,SAAS,EAA8D,MAAM,aAAa,CAAA;AAGxG,MAAM,MAAM,SAAS,GAAG;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB,CAAA;AAED,mEAAmE;AACnE,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+BAA+B;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,aAAa,GAAG,WAAW,CAAA;AAEvF,KAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;CAClB,CAAA;AA4HD,qBAAa,cAAe,SAAQ,eAAe,CACjD,YAAY,EACZ,mBAAmB,EACnB,SAAS,CAAC,MAAM,CAAC,EACjB,kBAAkB,CACnB;IACC,SAAkB,IAAI,EAAG,MAAM,CAAS;IACxC,QAAQ,CAAC,IAAI,EAAG,KAAK,CAAS;IAC9B,SAAkB,QAAQ,EAAG,KAAK,CAAS;IAC3C,SAAkB,GAAG,EAAG,KAAK,CAAS;IACtC,SAAkB,eAAe,YAAI,MAAM,EAAU;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,SAAS,CAA8B;IAE/C,YAAY,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAGjD;IAED,0FAA0F;IACpF,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC,CAO1C;IAED,kEAAkE;IAC5D,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAM/C;IAED,wEAAwE;IAClE,MAAM,CAAC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAMlE;IAED;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAK7B;IAED,yEAAyE;IACzE,OAAO,CAAC,MAAM;IAQd,6EAA6E;IAC7E,OAAO,CAAC,KAAK;IAOb,+EAA+E;IAC/E,OAAO,CAAC,IAAI;IAWZ,6DAA6D;YAC/C,IAAI;IA8BlB;;;;OAIG;YACW,KAAK;IAmDZ,UAAU,CAAC,OAAO,EAAE,WAAW,CAAC,mBAAmB,CAAC,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAiJ7F;IAED;;;;OAIG;IACG,gBAAgB,CACpB,OAAO,EAAE,uBAAuB,CAAC,mBAAmB,CAAC,GACpD,OAAO,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAmC1C;CACF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,cAAc,CAAA;AAC3D,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,cAAc,CAAA"}