apple-intelligence-models 0.2.0

AIMX: safe Rust bindings for Apple's on-device Apple Intelligence language models
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
# AIMX Tutorial

This tutorial teaches AIMX the way a good Rust crate should be learned: one
small invariant at a time, with examples that make the failure modes visible.
AIMX is the `apple-intelligence-models` package, imported in Rust as `aimx`.

By the end, you will have code that can:

1. Check whether Apple Intelligence is available.
2. Send a one-shot prompt.
3. Build a reusable session with instructions and generation options.
4. Stream response chunks.
5. Request structured JSON output.
6. Register a Rust tool that the model can call.
7. Run a tested gallery of agent use cases.
8. Handle unavailable hardware, invalid input, and model errors without panics.

## The Big Idea

The simplest way to understand AIMX is:

> Build a safe Rust boundary around the local Apple Intelligence model.

That boundary has three jobs.

1. Check availability before the user waits on work that cannot run.
2. Convert raw text, numbers, schemas, and tool arguments into typed Rust values.
3. Return typed errors instead of panicking when the platform, input, or model
   response is not usable.

This is the same shape you see in the standard Rust documentation: start with a
minimal working example, then explain the type that makes the example safe, then
show how errors are reported.

## Naming Map

AIMX uses a few names because Rust package names, Rust crate imports, and Apple
framework names serve different purposes.

| Role | Name |
|---|---|
| Project brand | AIMX |
| Cargo package | `apple-intelligence-models` |
| Rust import | `aimx` |
| Preferred model handle | `AppleIntelligenceModels` |
| Apple framework | `FoundationModels.framework` |
| Internal bridge cfg | `aimx_bridge` |

Use `AppleIntelligenceModels` in new application code. Compatibility aliases
such as `SystemLanguageModel`, `FoundationModels`, and `Client` still compile,
but examples and docs use the AIMX-first naming.

## Learning Path

The tutorial is ordered so each step introduces one new idea:

| Step | New concept | Why it matters |
|---|---|---|
| 1 | Availability | Avoid work the local platform cannot complete. |
| 2 | One-shot response | Learn the smallest async call. |
| 3 | Session builder | Put instructions and defaults in one visible place. |
| 4 | Typed options | Keep raw primitives at input boundaries. |
| 5 | Streaming | Handle incremental model output. |
| 6 | Structured output | Ask for JSON that maps to Rust types. |
| 7 | Tools | Let the model call recoverable Rust functions. |
| 8 | Tested agent examples | Keep examples deterministic even without Apple hardware. |
| 9 | Error handling | Turn platform and input failures into user-facing actions. |
| 10 | Integration | Combine the pieces into a small application. |

## Prerequisites

For normal compilation and tests:

| Requirement | Value |
|---|---|
| Rust | 1.85 or newer |
| Cargo | Included with Rust |
| OS | Any host that can compile Rust |

For live model responses:

| Requirement | Value |
|---|---|
| macOS | 26 (Tahoe) or newer |
| Hardware | Apple Silicon M1 or newer |
| System setting | Apple Intelligence enabled |
| SDK | Xcode with the macOS 26 SDK |

AIMX is intentionally graceful on unsupported hosts. If the Swift bridge cannot
be compiled, or the current Mac cannot run Apple Intelligence, model calls
return `Err(Error::Unavailable(...))` instead of panicking or failing at link
time.

## Create A Tutorial Project

Create a new Rust binary project:

```sh
cargo new aimx-tutorial
cd aimx-tutorial
```

Add AIMX and a small async runtime to `Cargo.toml`:

```toml
[dependencies]
aimx = { package = "apple-intelligence-models", version = "0.2" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
futures-util = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```

AIMX itself is runtime-agnostic. Tokio is used here only to keep the tutorial
binary short and familiar.

## Step 1: Check Availability

Start with availability. This lets your application show a good fallback before
the user waits on a model call that cannot run on the current machine.

```rust
use aimx::{availability, AvailabilityError};

fn print_availability() -> bool {
    match availability() {
        Ok(()) => {
            println!("Apple Intelligence is ready.");
            true
        }
        Err(AvailabilityError::DeviceNotEligible) => {
            eprintln!("This device cannot run the local Apple Intelligence model.");
            false
        }
        Err(AvailabilityError::NotEnabled) => {
            eprintln!("Enable Apple Intelligence in System Settings.");
            false
        }
        Err(AvailabilityError::ModelNotReady) => {
            eprintln!("The on-device model is still downloading or preparing.");
            false
        }
        Err(AvailabilityError::Unknown) => {
            eprintln!("Apple Intelligence is unavailable for an unknown reason.");
            false
        }
    }
}
```

The same check is also available on the preferred model handle:

```rust
use aimx::AppleIntelligenceModels;

let model = AppleIntelligenceModels::new();
let ready = model.is_available();
```

## Step 2: Send A One-Shot Prompt

Use `respond` for the smallest possible request. It creates a fresh session,
sends one prompt, and returns the model text as a `String`.

```rust
use aimx::{is_available, respond, Error};

#[tokio::main]
async fn main() -> Result<(), Error> {
    if !is_available() {
        eprintln!("Apple Intelligence is not available on this device.");
        return Ok(());
    }

    let answer = respond("Explain Rust ownership in one sentence.").await?;
    println!("{answer}");

    Ok(())
}
```

This is the right API for quick commands, checks, and scripts. If your program
needs instructions, tools, or repeated turns, build a session instead.

## Step 3: Build A Stateful Session

A `LanguageModelSession` keeps instructions and conversation context together.
Use the builder from `AppleIntelligenceModels::default().session()`.

```rust
use aimx::{AppleIntelligenceModels, Error, MaxTokens, Temperature};

async fn answer_rust_question() -> Result<(), Error> {
    let session = AppleIntelligenceModels::default()
        .session()
        .instructions("You are a concise Rust tutor. Prefer short examples.")
        .temperature(Temperature::new(0.2)?)
        .max_tokens(MaxTokens::new(256)?)
        .build()?;

    let first = session.respond_to("What problem does ownership solve?").await?;
    let second = session.respond_to("Show a minimal example.").await?;

    println!("{first}\n\n{second}");
    Ok(())
}
```

The builder also keeps Rig-style aliases for users coming from agent-builder
APIs:

```rust
use aimx::{AppleIntelligenceModels, Error, Temperature};

fn build_with_rig_style_names() -> Result<aimx::LanguageModelSession, Error> {
    AppleIntelligenceModels::default()
        .agent()
        .preamble("You are a careful code reviewer.")
        .temperature(Temperature::new(0.1)?)
        .build()
}
```

Prefer the Apple-style names in new docs and application code:

| Preferred | Compatibility alias |
|---|---|
| `session()` | `agent()` |
| `instructions(...)` | `preamble(...)` |
| `respond_to(...)` | `respond(...)` / `complete(...)` |
| `stream_response(...)` | `stream(...)` |
| `respond_generating(...)` | `respond_as(...)` |

## Step 4: Keep Raw Inputs At Boundaries

AIMX avoids meaning-bearing primitives in reusable domain state. Instead of
storing raw `f64` or `usize` values in your application model, parse them into
typed options as soon as they enter your code.

```rust
use aimx::{Error, GenerationOptions, MaxTokens, Temperature};

fn fixed_options() -> Result<GenerationOptions, Error> {
    Ok(GenerationOptions::new()
        .temperature(Temperature::new(0.2)?)
        .max_tokens(MaxTokens::new(512)?))
}
```

When values come from a CLI flag, UI form, or JSON payload, use the explicitly
fallible boundary methods:

```rust
use aimx::{Error, GenerationOptions};

fn options_from_user_input(temperature: f64, max_tokens: usize) -> Result<GenerationOptions, Error> {
    GenerationOptions::new()
        .try_temperature(temperature)?
        .try_max_tokens(max_tokens)
}
```

Invalid values become typed errors:

| Bad input | Error |
|---|---|
| `NaN` temperature | `Error::InvalidTemperature` |
| temperature outside the allowed range | `Error::InvalidTemperature` |
| too many tokens for the bridge | `Error::InvalidMaxTokens` |
| prompt or instructions containing `\0` | `Error::NullByte` |

## Step 5: Stream Response Text

Streaming returns a `ResponseStream`. Each item is a typed `ResponseText`, so
you can print it directly or call `as_str()` / `into_string()`.

```rust
use aimx::{Error, LanguageModelSession};
use futures_util::StreamExt as _;

async fn stream_story() -> Result<(), Error> {
    let session = LanguageModelSession::with_instructions(
        "Write vivid but concise responses.",
    )?;

    let mut stream = session.stream_response("Write a three-sentence story.")?;

    while let Some(chunk) = stream.next().await {
        let chunk = chunk?;
        print!("{}", chunk.as_str());
    }

    println!();
    Ok(())
}
```

You can also use MLX-style names when you want model-inference terminology:

```rust
use aimx::{AppleIntelligenceModels, Error};

fn start_generation_stream() -> Result<aimx::ResponseStream, Error> {
    AppleIntelligenceModels::default()
        .session()
        .build()?
        .stream_generate("List three reasons Rust is useful for FFI.")
}
```

## Step 6: Generate Structured Output

Use a `GenerationSchema` when you want JSON that deserializes into your own Rust
type. Field names in the schema must match the fields in the Rust type.

```rust
use aimx::{
    Error, GenerationSchema, GenerationSchemaProperty, GenerationSchemaPropertyType,
    LanguageModelSession,
};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct ReleaseNote {
    title: String,
    summary: String,
    risk: String,
}

fn release_note_schema() -> GenerationSchema {
    GenerationSchema::new("ReleaseNote")
        .description("A short engineering release note")
        .property(
            GenerationSchemaProperty::new("title", GenerationSchemaPropertyType::String)
                .description("A concise title"),
        )
        .property(
            GenerationSchemaProperty::new("summary", GenerationSchemaPropertyType::String)
                .description("What changed"),
        )
        .property(
            GenerationSchemaProperty::new("risk", GenerationSchemaPropertyType::String)
                .description("Main release risk or fallback"),
        )
}

async fn write_release_note() -> Result<ReleaseNote, Error> {
    let session = LanguageModelSession::new()?;
    session
        .respond_generating(
            "Summarize a refactor that renamed a crate to AIMX.",
            &release_note_schema(),
        )
        .await
}
```

Schema properties are typed. The public schema model uses
`GenerationSchemaPropertyRequirement` instead of a raw boolean for optionality.

```rust
use aimx::{
    GenerationSchemaProperty, GenerationSchemaPropertyRequirement, GenerationSchemaPropertyType,
};

let property = GenerationSchemaProperty::new("details", GenerationSchemaPropertyType::String)
    .required(GenerationSchemaPropertyRequirement::Optional);
```

## Step 7: Register A Rust Tool

Tools let the model ask your Rust code for data while forming a response. The
safe mental model is to treat a tool like a small API endpoint: it has a name, a
description, a schema for its arguments, and a handler that can fail normally.

The handler returns `ToolOutput` on success or `ToolCallError` on failure. That
keeps tool failures inside the model/tool protocol instead of panicking.

```rust
use aimx::{
    AppleIntelligenceModels, Error, GenerationSchema, GenerationSchemaProperty,
    GenerationSchemaPropertyType, ToolCallError, ToolDefinition, ToolOutput,
};
use serde_json::Value;

fn weather_tool() -> ToolDefinition {
    let parameters = GenerationSchema::new("WeatherArgs")
        .description("Arguments for a weather lookup")
        .property(
            GenerationSchemaProperty::new("city", GenerationSchemaPropertyType::String)
                .description("City name"),
        );

    ToolDefinition::builder(
        "get_weather",
        "Return current weather for a city",
        parameters,
    )
    .handler(|args: Value| {
        let city = args
            .get("city")
            .and_then(Value::as_str)
            .ok_or_else(|| ToolCallError::new("missing string field: city"))?;

        Ok(ToolOutput::from(format!("{city}: 22 C, sunny")))
    })
}

async fn answer_with_tool() -> Result<(), Error> {
    let session = AppleIntelligenceModels::default()
        .session()
        .instructions("Use tools when factual lookup is needed.")
        .tool(weather_tool())
        .build()?;

    let answer = session.respond_to("What is the weather in Tokyo?").await?;
    println!("{answer}");

    Ok(())
}
```

Tool handlers are user code. AIMX catches handler panics before they can cross
the tool boundary, but production tools should still return explicit
`ToolCallError` values for bad arguments, missing local data, or disabled
features.

## Step 8: Explore Tested Agent Use Cases

The repository includes a deterministic agent gallery in
[`examples/agent_use_cases.rs`](examples/agent_use_cases.rs). It demonstrates a
variety of LLM-agent patterns without requiring live Apple Intelligence for the
unit tests.

Run the example:

```sh
cargo run --example agent_use_cases
```

Run its unit tests:

```sh
cargo test --example agent_use_cases
```

The example covers:

| Use case | Pattern | Tested contract |
|---|---|---|
| `research_brief` | plain text synthesis | prompt, instructions, and options validate |
| `support_triage` | structured routing | schema serializes `priority`, `team`, and `summary` |
| `code_review` | review assistant | prompt keeps typed-error and callback-risk semantics |
| `release_notes` | structured writing | schema marks `risk` optional |
| `meeting_actions` | extraction | schema serializes `owner`, `action`, and `due_date` |
| `weather_tool` | tool-augmented agent | tool returns `Tokyo: 22 C, sunny` and typed errors for bad args |

The tests assert expected deterministic results at the Rust boundary. They do
not assert live model text, because model output depends on local system state,
model availability, and prompt sampling. To run the live research-brief path
manually on supported hardware:

```sh
AIMX_RUN_LIVE_AGENT_EXAMPLES=1 cargo run --example agent_use_cases
```

Use this pattern for your own agents: test prompt construction, schemas, tool
handlers, and error mapping locally, then keep live generation as an explicit
integration test.

## Step 9: Handle Errors Smoothly

Most application entry points can return `Result<(), aimx::Error>` and use `?`.
At UX boundaries, match on errors so the caller sees the next useful action.

```rust
use aimx::{AvailabilityError, Error};

fn explain_error(error: Error) -> String {
    match error {
        Error::Unavailable(AvailabilityError::DeviceNotEligible) => {
            "This device does not support the local Apple Intelligence model.".to_string()
        }
        Error::Unavailable(AvailabilityError::NotEnabled) => {
            "Apple Intelligence is disabled in System Settings.".to_string()
        }
        Error::Unavailable(AvailabilityError::ModelNotReady) => {
            "The local model is not ready yet. Try again after setup finishes.".to_string()
        }
        Error::Unavailable(AvailabilityError::Unknown) => {
            "Apple Intelligence is unavailable for an unknown reason.".to_string()
        }
        Error::NullByte(_) => {
            "The prompt contains a NUL byte and cannot cross the C FFI boundary.".to_string()
        }
        Error::InvalidTemperature(value) => {
            format!("Temperature {value} is outside the supported range.")
        }
        Error::InvalidMaxTokens(value) => {
            format!("max_tokens value {value} is too large for the bridge.")
        }
        Error::Json(error) => format!("Structured generation failed JSON handling: {error}"),
        Error::Generation(error) => format!("Model generation failed: {error}"),
    }
}
```

Avoid `panic!`, `unwrap`, and `expect` in application paths that process user
input, model output, or tool arguments. AIMX exposes typed constructors and
typed errors so those cases can stay recoverable.

## Step 10: Put It Together

Here is a compact application that checks availability, builds a session, and
uses typed generation options.

```rust
use aimx::{
    AppleIntelligenceModels, AvailabilityError, Error, GenerationOptions, MaxTokens,
    Temperature,
};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let model = AppleIntelligenceModels::new();

    if let Err(reason) = model.availability() {
        print_unavailable(reason);
        return Ok(());
    }

    let options = GenerationOptions::new()
        .temperature(Temperature::new(0.2)?)
        .max_tokens(MaxTokens::new(256)?);

    let session = model
        .session()
        .instructions("You are a concise assistant for Rust engineers.")
        .options(options)
        .build()?;

    let answer = session
        .respond_to("Explain why typed FFI boundaries matter.")
        .await?;

    println!("{answer}");
    Ok(())
}

fn print_unavailable(reason: AvailabilityError) {
    match reason {
        AvailabilityError::DeviceNotEligible => {
            eprintln!("AIMX requires Apple Silicon for live local generation.")
        }
        AvailabilityError::NotEnabled => {
            eprintln!("Enable Apple Intelligence before running live generation.")
        }
        AvailabilityError::ModelNotReady => {
            eprintln!("Apple Intelligence is present but the model is not ready.")
        }
        AvailabilityError::Unknown => {
            eprintln!("Apple Intelligence is unavailable for an unknown reason.")
        }
    }
}
```

## Step 11: Test Code That Does Not Need Hardware

You can test validation and public API behavior on any host because AIMX returns
typed errors before it reaches the live model.

```rust
use aimx::{AppleIntelligenceModels, Error, MaxTokens, Prompt, Temperature};
use futures_executor::block_on;

#[test]
fn prompt_rejects_nul_byte() {
    assert!(matches!(
        Prompt::try_from("bad\0prompt"),
        Err(Error::NullByte(_))
    ));
}

#[test]
fn options_validate_ranges() -> Result<(), Error> {
    Temperature::new(0.3)?;
    MaxTokens::new(256)?;
    assert!(matches!(
        Temperature::new(f64::NAN),
        Err(Error::InvalidTemperature(_))
    ));
    Ok(())
}

#[test]
fn model_rejects_bad_prompt_before_availability() {
    let result = block_on(AppleIntelligenceModels::default().respond("bad\0prompt"));
    assert!(matches!(result, Err(Error::NullByte(_))));
}
```

Live model tests should be ignored by default and enabled explicitly on a
supported Mac:

```rust
#[test]
#[ignore = "requires Apple Intelligence"]
fn live_model_smoke_test() {
    let answer = futures_executor::block_on(aimx::respond("Say hello."));
    assert!(answer.is_ok());
}
```

Run normal tests everywhere:

```sh
cargo test
```

Run live tests only on supported hardware:

```sh
cargo test -- --include-ignored
```

## Step 12: Troubleshooting

### `Error::Unavailable(DeviceNotEligible)`

The crate compiled, but the current host is not eligible for live local
generation. This is expected on non-macOS machines, older macOS versions, Intel
Macs, or CI runners without Apple Intelligence.

### `Error::Unavailable(NotEnabled)`

Apple Intelligence is available but disabled. Enable it in System Settings.

### `Error::Unavailable(ModelNotReady)`

The system reports that the model is not ready. Wait for the system model setup
or download to finish, then retry.

### Swift Bridge Did Not Compile

`build.rs` attempts to compile `bridge.swift` with `xcrun swiftc`. If Xcode or
the macOS 26 SDK is missing, the build continues without `aimx_bridge`.
Application code still compiles, and model APIs return `Error::Unavailable`.

### Structured Output Fails To Deserialize

Check that every field in your Rust type has the same name and compatible type
as the property in your `GenerationSchema`. Start with a small schema and add
fields one at a time.

### Tool Handler Returns An Error

Treat tool errors as normal model workflow. Validate model-supplied JSON with
`Value::get`, `Value::as_*`, and typed error messages. Return
`ToolCallError::new(...)` instead of panicking.

## Step 13: Production Checklist

Before publishing or shipping an AIMX integration:

1. Keep user input conversion at typed boundaries (`Prompt`, `SystemInstructions`,
   `Temperature`, `MaxTokens`, `GenerationSchema`).
2. Match on `Error::Unavailable` and provide a local fallback path.
3. Keep live Apple Intelligence tests ignored by default.
4. Run `cargo fmt --check`.
5. Run `cargo test`.
6. Run `cargo test --examples`.
7. Run `cargo clippy --all-targets --all-features -- -D warnings`.
8. Run `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps`.
9. Run `cargo publish --dry-run` before publishing a crate release.

## Next Steps

After this tutorial, read:

- [README.md]README.md for the concise API overview.
- [references/documentation-style.md]references/documentation-style.md for the
  style expected in repository docs and public rustdoc.
- [references/async-architecture.md]references/async-architecture.md for the
  callback, cancellation, stream, and tool-handler boundaries.
- [references/policy.md]references/policy.md for the primitive-boundary policy.
- [CONTRIBUTING.md]CONTRIBUTING.md before opening changes against AIMX.