@tanstack-ai-browser/afm 0.0.0-stage → 0.2.2

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/Package.swift +33 -0
  3. package/README.md +75 -2
  4. package/dist/adapter.d.ts +107 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +522 -0
  7. package/dist/electron.d.ts +29 -0
  8. package/dist/electron.d.ts.map +1 -0
  9. package/dist/electron.js +136 -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/json-value.d.ts +9 -0
  14. package/dist/json-value.d.ts.map +1 -0
  15. package/dist/json-value.js +19 -0
  16. package/dist/messages.d.ts +12 -0
  17. package/dist/messages.d.ts.map +1 -0
  18. package/dist/messages.js +29 -0
  19. package/dist/model-meta.d.ts +23 -0
  20. package/dist/model-meta.d.ts.map +1 -0
  21. package/dist/model-meta.js +20 -0
  22. package/dist/native.d.ts +175 -0
  23. package/dist/native.d.ts.map +1 -0
  24. package/dist/native.js +21 -0
  25. package/dist/schema.d.ts +17 -0
  26. package/dist/schema.d.ts.map +1 -0
  27. package/dist/schema.js +133 -0
  28. package/dist/validate.d.ts +2 -0
  29. package/dist/validate.d.ts.map +1 -0
  30. package/dist/validate.js +124 -0
  31. package/electron/AfmNode.swift +81 -0
  32. package/electron/afm.c +94 -0
  33. package/electron/afm.node +0 -0
  34. package/electron/afm.node.sha256 +1 -0
  35. package/electron/build.mjs +54 -0
  36. package/electron/preload.cjs +35 -0
  37. package/electron/sources-hash.mjs +40 -0
  38. package/ios/Sources/AfmCore/Bridge.swift +354 -0
  39. package/ios/Sources/AfmCore/Conversation.swift +49 -0
  40. package/ios/Sources/AfmCore/Schema.swift +90 -0
  41. package/ios/Sources/AfmPlugin/AfmPlugin.swift +89 -0
  42. package/package.json +97 -3
  43. package/src/adapter.test.ts +428 -0
  44. package/src/adapter.ts +616 -0
  45. package/src/electron.ts +193 -0
  46. package/src/index.ts +28 -0
  47. package/src/json-value.test.ts +26 -0
  48. package/src/json-value.ts +17 -0
  49. package/src/messages.ts +37 -0
  50. package/src/model-meta.ts +37 -0
  51. package/src/native-build.test.ts +13 -0
  52. package/src/native.ts +128 -0
  53. package/src/schema.test.ts +146 -0
  54. package/src/schema.ts +153 -0
  55. package/src/validate.test.ts +38 -0
  56. package/src/validate.ts +112 -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,76 @@
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
+ // One threadId per conversation: its follow-ups reuse the same native session.
28
+ const stream = chat({ adapter, threadId, messages, tools })
29
+ }
30
+ ```
31
+
32
+ `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.
33
+
34
+ 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.
35
+
36
+ ### Guardrails
37
+
38
+ Apple's default guardrails can block ordinary text that sounds unsafe out of context, such as a card game's card names ("add 2 Searing Blood", "swap the Shocks for Play with Fire"), with a `guardrailViolation` error. `guardrails: "permissiveContentTransformations"` relaxes them:
39
+
40
+ ```ts
41
+ const adapter = afmText({ guardrails: "permissiveContentTransformations" })
42
+ ```
43
+
44
+ A request's `modelOptions.guardrails` overrides the adapter's, and in Electron `serveAfm({ guardrails })` sets them for every page that names none. `"default"` is the default.
45
+
46
+ Apple's permissive guardrails cover text responses only: anything else, such as structured output (`chat({ outputSchema })`), gets the default guardrails. So when a permissive session's structured answer is blocked, the adapter asks for it again as text: a JSON value to the schema. The framework doesn't hold that answer to the schema while it's written, so the adapter parses the whole answer (or everything inside a code fence) and checks it against the schema before returning it; one that doesn't fit fails with the code `schemaMismatch`, and one that isn't JSON with `invalidJson`. (`chat()` itself validates only schemas that come with a Standard Schema validator, not plain JSON Schema.) Tool calls' arguments keep the default guardrails. A guardrail violation that still happens is reported with the code `guardrailViolation`.
47
+
48
+ ### Electron (macOS)
49
+
50
+ The page is the same as on iOS: `afmText()`. The main process runs the model, with one call before opening windows:
51
+
52
+ ```ts
53
+ // electron/main.js
54
+ import { app, BrowserWindow } from "electron"
55
+ import { serveAfm } from "@tanstack-ai-browser/afm"
56
+
57
+ serveAfm()
58
+ app.whenReady().then(() => new BrowserWindow().loadFile("index.html"))
59
+ ```
60
+
61
+ `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.
62
+
63
+ `serveAfm({ directory, allow })`:
64
+
65
+ - `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`).
66
+ - `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.
67
+
68
+ 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.
69
+
70
+ `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.
71
+
72
+ ### Another host
73
+
74
+ Pass any implementation of `AfmNative` (the plugin's methods and its `event` listener) as `native`, or put it at `window.afm`.
75
+
76
+ 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,107 @@
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 { AfmGuardrails, 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
+ * Apple's guardrails: "default" (the default), or "permissiveContentTransformations",
15
+ * for text the default ones block out of context, such as a card game's card
16
+ * names. A request's `modelOptions.guardrails` overrides it.
17
+ */
18
+ guardrails?: AfmGuardrails;
19
+ };
20
+ /** Per-call `modelOptions`: the response's sampling and length, and the guardrails. */
21
+ export type AfmTextModelOptions = {
22
+ temperature?: number;
23
+ /** Most tokens to generate. */
24
+ maximumResponseTokens?: number;
25
+ /** Apple's guardrails for this request's session, over the adapter's `guardrails`. */
26
+ guardrails?: AfmGuardrails;
27
+ };
28
+ export type Availability = "unavailable" | "downloadable" | "downloading" | "available";
29
+ type MetadataByModality = {
30
+ text: unknown;
31
+ image: unknown;
32
+ audio: unknown;
33
+ video: unknown;
34
+ document: unknown;
35
+ };
36
+ export declare class AfmTextAdapter extends BaseTextAdapter<AfmChatModel, AfmTextModelOptions, readonly ["text"], MetadataByModality> {
37
+ readonly kind: "text";
38
+ readonly name: "afm";
39
+ readonly provider: "afm";
40
+ readonly api: "afm";
41
+ readonly inputModalities: readonly ["text"];
42
+ private readonly native;
43
+ private readonly threads;
44
+ private readonly queues;
45
+ /**
46
+ * Responses waiting for tool results in sessions no thread keeps (prompt
47
+ * cache retention "none"), by thread: the next step resumes them.
48
+ */
49
+ private readonly paused;
50
+ private listening;
51
+ constructor(config: AfmConfig, model: AfmChatModel);
52
+ private readonly guardrails;
53
+ /** A request's guardrails: its own, else the adapter's (left out: the host's, else Apple's default). */
54
+ private guardrailsOf;
55
+ /** Whether Apple's model can run here: "downloading" while Apple Intelligence gets it. */
56
+ availability(): Promise<Availability>;
57
+ /** Tokens a session holds, where the OS says (26.4 and later). */
58
+ contextSize(): Promise<number | undefined>;
59
+ /** Resolves to this adapter if the model can run here, else rejects. */
60
+ create(options?: {
61
+ signal?: AbortSignal;
62
+ }): Promise<this>;
63
+ /**
64
+ * Ends every session: the threads', responses waiting for tool results, and
65
+ * responses streaming now (structured output's, and those of sessions no
66
+ * thread keeps), each once.
67
+ */
68
+ destroy(): Promise<void>;
69
+ /** Routes the native events to their thread's response. Listens once. */
70
+ private listen;
71
+ /** Ends a session. A response still streaming from it ends with an error. */
72
+ private close;
73
+ /** Keeps a thread's session, ending the least recently used past the limit. */
74
+ private keep;
75
+ /** A new native session for a thread, holding `messages`. */
76
+ private open;
77
+ /**
78
+ * The step's thread, and what it sends: the results of the tool calls its
79
+ * response waits on, a new prompt to its session, or (for a conversation the
80
+ * session doesn't hold) a new session and the last message as its prompt.
81
+ */
82
+ private begin;
83
+ chatStream(options: TextOptions<AfmTextModelOptions>): AsyncIterable<AdapterYieldChunk>;
84
+ /**
85
+ * A JSON answer for `chat({ outputSchema })`: a session holding the
86
+ * conversation answers its last message, held to the schema by the
87
+ * framework. `chat()` validates the result.
88
+ */
89
+ structuredOutput(options: StructuredOutputOptions<AfmTextModelOptions>): Promise<StructuredOutputResult<unknown>>;
90
+ }
91
+ /**
92
+ * A TanStack AI text adapter for Apple's on-device model, through the
93
+ * Foundation Models framework (the Capacitor plugin on iOS, by default).
94
+ * The model is Apple's system model ("system"), so naming it is optional.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * const adapter = afmText()
99
+ * if ((await adapter.availability()) === "available") {
100
+ * chat({ adapter, messages, tools })
101
+ * }
102
+ * ```
103
+ */
104
+ export declare function afmText(config?: AfmConfig): AfmTextAdapter;
105
+ export declare function afmText(model: AfmChatModel, config?: AfmConfig): AfmTextAdapter;
106
+ export {};
107
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}