Skip to content
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,10 @@ Icon
# Local git worktrees (Claude / superpowers convention)
.worktrees/
Vendor/

# Opt-in Core AI device proof
Examples/CoreAIProof/Tests/CoreAIProofTests/Resources/Qwen3-0.6B/
Examples/CoreAIProof/Reports/
Examples/CoreAIProof/.build/
Examples/CoreAIProof/.swiftpm/
Examples/CoreAIProof/Package.resolved
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and Aria adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.12.0] - 2026-08-30

### Added

- **Custom Apple `LanguageModel` injection on iOS 27 and related platform releases.**
`FoundationModelsProvider` can now construct sessions from any Foundation
Models `LanguageModel`, including models backed by Core AI, while retaining
the existing transcript, tools, streaming, structured-output, and error
handling paths. Aria validates declared model capabilities before generation
and leaves model loading, routing, and fallback policy to the host app.
- **An isolated Core AI device proof.** The example package demonstrates the
integration boundary without adding Core AI or model assets to Aria's core
dependency graph.

### Changed

- Foundation Models session creation now flows through one internal factory so
the system model and injected models share the same execution behavior.

## [0.1.4] - 2026-05-26

### Added
Expand Down
31 changes: 31 additions & 0 deletions Examples/CoreAIProof/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// swift-tools-version: 6.4

import PackageDescription

let package = Package(
name: "CoreAIProof",
platforms: [
.iOS(.v27),
],
dependencies: [
.package(name: "aria", path: "../.."),
.package(
url: "https://github.com/apple/coreai-models.git",
revision: "de31ba508895c7aa3bdcc57f8837a23f13316871"
),
],
targets: [
.testTarget(
name: "CoreAIProofTests",
dependencies: [
.product(name: "Aria", package: "aria"),
.product(name: "AriaApple", package: "aria"),
.product(name: "AriaTesting", package: "aria"),
.product(name: "CoreAILM", package: "coreai-models"),
],
resources: [
.copy("Resources"),
]
),
]
)
60 changes: 60 additions & 0 deletions Examples/CoreAIProof/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Core AI through Aria device proof

This opt-in package verifies that a Core AI `CoreAILanguageModel` can use Aria's
text streaming, guided generation, typed tools, and task-evaluation surfaces. It
is deliberately outside Aria's root package graph and is not a benchmark.

## Requirements

- Xcode 27 or newer.
- A physical iPhone running iOS 27 or newer.
- `uv` and a checkout of Apple's
[`coreai-models`](https://github.com/apple/coreai-models) repository.

Core AI is unavailable in the iOS Simulator SDK. The pinned upstream package
cannot currently compile when a Core AI product is linked for a simulator
destination, so use a generic iOS or connected-device destination only.

## Prepare the model

From the `coreai-models` checkout, export Qwen3-0.6B:

```bash
uv run coreai.llm.export Qwen/Qwen3-0.6B --platform iOS --output-dir ./exported-models
```

Copy the exported model resource folder to:

```text
Tests/CoreAIProofTests/Resources/Qwen3-0.6B/
```

The copied directory is ignored by Git. It must contain the export's
`metadata.json`, model asset, and tokenizer resources.

## Compile the proof

From this directory:

```bash
DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer \
xcodebuild build-for-testing \
-scheme CoreAIProof-Package \
-destination 'generic/platform=iOS' \
-skipPackagePluginValidation \
-skipMacroValidation
```

## Run on a device

1. Open this directory's `Package.swift` in Xcode 27.
2. Select a connected iPhone running iOS 27 or newer.
3. Add `COREAI_ARIA_PROOF=1` to the test scheme's environment variables.
4. Run all `CoreAIProofTests` tests.

Without the environment gate, the suite skips before looking for model assets.
With it enabled, missing resources fail with the expected destination path.

Each case prints a `ContinuousClock` duration, and the task-evaluation case
prints its `TaskEval` summary. These values are diagnostic evidence only: there
are no performance thresholds and no Core AI versus MLX comparison.
186 changes: 186 additions & 0 deletions Examples/CoreAIProof/Tests/CoreAIProofTests/CoreAIProofTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import Aria
import AriaApple
import AriaTesting
import CoreAILanguageModels
import Foundation
import FoundationModels
import XCTest

@available(iOS 27.0, *)
@Generable
private struct ProofStructuredResponse {
@Guide(description: "A short confirmation status")
var status: String

@Guide(description: "A short diagnostic detail")
var detail: String
}

@available(iOS 27.0, *)
final class CoreAIProofTests: XCTestCase {
override func setUp() async throws {
try await super.setUp()
try XCTSkipUnless(
ProcessInfo.processInfo.environment["COREAI_ARIA_PROOF"] == "1",
"Set COREAI_ARIA_PROOF=1 to run the physical-device proof"
)

let resources = try XCTUnwrap(
Bundle.module.url(
forResource: "Qwen3-0.6B",
withExtension: nil,
subdirectory: "Resources"
),
"Export Qwen3-0.6B and copy it into the proof Resources directory"
)
let model = try await CoreAILanguageModel(resourcesAt: resources, mode: .eager)
self.model = model
self.capabilities = ProviderCapabilities(
modelIdentifier: "coreai.qwen3-0.6b",
supportsToolUse: model.capabilities.contains(.toolCalling),
supportsStructuredOutput: model.capabilities.contains(.guidedGeneration)
)
self.toolKit = registerFoundationModelsTool(ProofTool())
}

private var model: CoreAILanguageModel?
private var capabilities: ProviderCapabilities?
private var toolKit: FoundationModelsToolKit?

private func provider(includeProofTool: Bool = false) throws -> FoundationModelsProvider {
let model = try XCTUnwrap(self.model)
let capabilities = try XCTUnwrap(self.capabilities)
let toolKit = try XCTUnwrap(self.toolKit)
return FoundationModelsProvider(
model: model,
defaultInstructions: "Follow the diagnostic request exactly and answer concisely.",
capabilities: capabilities,
typedTools: includeProofTool ? [toolKit.factory] : []
)
}

private func printTiming(_ label: String, since start: ContinuousClock.Instant) {
print("CoreAIProof \(label): \(ContinuousClock.now - start)")
}
}

@available(iOS 27.0, *)
extension CoreAIProofTests {
func testTextStreamsThroughAria() async throws {
let started = ContinuousClock.now
defer { self.printTiming("text", since: started) }

var sawStart = false
var text = ""
var sawStop = false
for try await event in try self.provider().stream(
messages: [.user("Reply with one short sentence confirming this Core AI request.")],
tools: [],
options: GenerationOptions(maxTokens: 64)
) {
switch event {
case .messageStart:
sawStart = true
case let .textDelta(delta):
text += delta
case .messageStop:
sawStop = true
default:
break
}
}

XCTAssertTrue(sawStart, "Expected Aria's provider start event")
XCTAssertFalse(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
XCTAssertTrue(sawStop, "Expected Aria's provider stop event")
}

func testStructuredOutputThroughAria() async throws {
let started = ContinuousClock.now
defer { self.printTiming("structured", since: started) }

var sawPartial = false
var final: ProofStructuredResponse?
for try await event in try self.provider().streamStructured(
messages: [.user("Return a status and detail confirming the Core AI Aria proof.")],
as: ProofStructuredResponse.self
) {
switch event {
case .partial:
sawPartial = true
case let .finish(content):
final = content
case .toolCallExecuted:
break
}
}

XCTAssertTrue(sawPartial, "Expected at least one partial structured value")
let output = try XCTUnwrap(final)
XCTAssertFalse(output.status.isEmpty)
XCTAssertFalse(output.detail.isEmpty)
}

func testToolExecutionThroughAria() async throws {
let started = ContinuousClock.now
defer { self.printTiming("tool", since: started) }

var output: ProofToolOutput?
let toolKit = try XCTUnwrap(self.toolKit)
for try await event in try self.provider(includeProofTool: true).stream(
messages: [
.user(
"Call coreai_aria_proof exactly once with request device_integration, "
+ "then report the returned marker."
),
],
executableTools: [toolKit.anyTool],
options: GenerationOptions(maxTokens: 128)
) {
guard case let .toolCallExecuted(call, result) = event,
call.name == ProofTool.name else {
continue
}
output = try result.output.decode(ProofToolOutput.self)
}

XCTAssertEqual(output?.marker, "COREAI_ARIA_TOOL_OK")
}

func testTaskEvalRecordsDiagnosticResult() async throws {
let started = ContinuousClock.now
defer { self.printTiming("task-eval", since: started) }

let model = try XCTUnwrap(self.model)
let capabilities = try XCTUnwrap(self.capabilities)
let toolKit = try XCTUnwrap(self.toolKit)
let testCase = TaskCase(
query: "Call coreai_aria_proof once and report the returned marker.",
tools: [toolKit.anyTool],
expectedTool: ProofTool.name,
mustContain: ["COREAI_ARIA_TOOL_OK"],
note: "One-trial integration diagnostic; semantic misses are recorded, not gated."
)
let report = await TaskEval(cases: [testCase], trials: 1).run(
label: "Core AI through Aria"
) { testCase in
Agent(config: AgentConfig(
provider: FoundationModelsProvider(
model: model,
defaultInstructions: "Use the requested diagnostic tool and report its result.",
capabilities: capabilities,
typedTools: [toolKit.factory]
),
tools: testCase.tools,
systemPrompt: "Report only results returned by the diagnostic tool."
))
}

print("\n\(report.summary())\n")
XCTAssertEqual(report.outcomes.count, 1)
XCTAssertNil(
report.outcomes.first?.error,
"The diagnostic records semantic misses but rejects infrastructure errors"
)
}
}
33 changes: 33 additions & 0 deletions Examples/CoreAIProof/Tests/CoreAIProofTests/ProofTool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Aria
import AriaApple
import FoundationModels

@available(iOS 27.0, *)
@Generable
struct ProofToolInput: Codable {
@Guide(description: "The exact diagnostic request to acknowledge")
var request: String
}

struct ProofToolOutput: Codable, Equatable, Sendable {
let marker: String
}

@available(iOS 27.0, *)
struct ProofTool: GenerableTool {
typealias Input = ProofToolInput
typealias Output = ProofToolOutput

static let name = "coreai_aria_proof"
static let description = "Returns the deterministic Core AI through Aria proof marker."
static let inputSchema = JSONSchema.object(
properties: [
"request": .string(description: "The diagnostic request to acknowledge."),
],
required: ["request"]
)

func call(_: ProofToolInput, context _: ToolContext) async throws -> ProofToolOutput {
ProofToolOutput(marker: "COREAI_ARIA_TOOL_OK")
}
}
16 changes: 16 additions & 0 deletions Examples/CoreAIProof/Tests/CoreAIProofTests/Resources/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Core AI proof model resources

Export Qwen3-0.6B from a checkout of Apple's `coreai-models` repository:

```bash
uv run coreai.llm.export Qwen/Qwen3-0.6B --platform iOS --output-dir ./exported-models
```

Copy the exported resource folder itself to:

```text
Tests/CoreAIProofTests/Resources/Qwen3-0.6B/
```

The final directory must contain the export's `metadata.json`, model asset, and
tokenizer resources. Model files are intentionally ignored by Git.
Loading
Loading