Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: CI

on:
pull_request:
push:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Build and test (default traits)
runs-on: macos-15
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

- name: Select latest Xcode
run: |
XCODE_DEV="$(ls -d /Applications/Xcode*.app | sort -V | tail -n 1)/Contents/Developer"
sudo xcode-select -s "$XCODE_DEV"
echo "DEVELOPER_DIR=$XCODE_DEV" >> "$GITHUB_ENV"
swift --version

- name: Cache SwiftPM build
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-default-${{ hashFiles('Package.swift', 'Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-default-

- name: Run tests
run: swift test

- name: Benchmark leakage check
run: python3 Scripts/check_benchmark_leakage.py

- name: Validate eval datasets
run: swift run memory_eval validate-datasets --strict

trait-build:
name: Build (CoreML + NaturalLanguage traits)
runs-on: macos-15
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

- name: Select latest Xcode
run: |
XCODE_DEV="$(ls -d /Applications/Xcode*.app | sort -V | tail -n 1)/Contents/Developer"
sudo xcode-select -s "$XCODE_DEV"
echo "DEVELOPER_DIR=$XCODE_DEV" >> "$GITHUB_ENV"
swift --version

- name: Cache SwiftPM build
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-traits-${{ hashFiles('Package.swift', 'Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-traits-

- name: Build with traits
run: swift build --traits CoreMLEmbedding,MemoryNaturalLanguage
55 changes: 55 additions & 0 deletions .github/workflows/evals.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Evals

# Manual-only: eval results are OS-sensitive (NaturalLanguage models vary by
# macOS version), so these run on demand for diagnostics rather than as a
# PR-blocking gate. The authoritative release gate remains local runs against
# Evals/baselines/current.json per AGENTS.md.
on:
workflow_dispatch:

jobs:
extraction-suites:
name: Extraction-affected eval suites (coreml_default)
runs-on: macos-15
timeout-minutes: 90
steps:
- uses: actions/checkout@v4

- name: Select latest Xcode
run: |
XCODE_DEV="$(ls -d /Applications/Xcode*.app | sort -V | tail -n 1)/Contents/Developer"
sudo xcode-select -s "$XCODE_DEV"
echo "DEVELOPER_DIR=$XCODE_DEV" >> "$GITHUB_ENV"
swift --version

- name: Cache SwiftPM build
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-traits-${{ hashFiles('Package.swift', 'Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-traits-

- name: Run extraction-affected suites
run: |
for ds in memory_schema_gold_v2 agent_memory_gold_v1 agent_memory_pressure_v1 eval_quality_smoke_v1 storage_heldout_v1; do
echo "=== $ds ==="
swift run --traits CoreMLEmbedding,MemoryNaturalLanguage memory_eval run \
--profile coreml_default --dataset-root "./Evals/$ds" --no-cache --no-index-cache
done

- name: Gate focused baselines
run: |
pressure_run=$(ls -t Evals/agent_memory_pressure_v1/runs/*-coreml_default.json | head -1)
smoke_run=$(ls -t Evals/eval_quality_smoke_v1/runs/*-coreml_default.json | head -1)
swift run --traits CoreMLEmbedding,MemoryNaturalLanguage memory_eval gate \
--baseline Evals/baselines/pressure.json "$pressure_run"
swift run --traits CoreMLEmbedding,MemoryNaturalLanguage memory_eval gate \
--baseline Evals/baselines/eval_quality_smoke.json "$smoke_run"

- name: Upload run reports
uses: actions/upload-artifact@v4
with:
name: eval-runs
path: Evals/*/runs/*coreml_default.*
if-no-files-found: warn
2 changes: 2 additions & 0 deletions Evals/agent_memory_gold_v1/scenarios.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@
{"id": "context-prep-location-protects-profile", "source_family": "context_preparation", "difficulty": "medium", "generation_method": "seed", "setup_memories": [{"text": "The user lives in San Francisco, CA.", "kind": "profile", "status": "active", "canonical_key": "profile:user:location", "facet_tags": ["fact_about_user", "location"], "entity_values": ["san francisco"], "topics": []}, {"text": "The user previously researched Austin restaurants.", "kind": "episode", "status": "active", "canonical_key": "episode:austin-restaurants", "facet_tags": [], "entity_values": ["austin"], "topics": ["austin restaurants"]}], "setup_context_hints": [{"path_prefix": "memory://", "context": "Memory records are durable user-facing facts."}], "messages": [{"role": "user", "content": "What should I do tonight in San Francisco?"}], "expected_write_count": 0, "expected_memories": [], "context_expectations": [{"query": "What should I do tonight in San Francisco?", "max_references": 4, "max_tokens": 256, "expected_text_contains": ["San Francisco"], "forbidden_text_contains": ["Austin"], "expected_hint_contains": ["durable user-facing facts"], "require_untrusted_framing": true}]}
{"id": "maintenance-repeated-recall-promotes-profile", "source_family": "maintenance", "difficulty": "medium", "generation_method": "seed", "setup_memories": [{"text": "The user prefers ramen for casual dinners.", "kind": "profile", "status": "active", "canonical_key": "profile:user:preference:ramen", "facet_tags": ["fact_about_user", "preference"], "entity_values": [], "topics": ["ramen"]}], "messages": [{"role": "user", "content": "Please do not save anything from this turn."}], "expected_write_count": 0, "expected_memories": [], "maintenance_expectation": {"signal_memory_canonical_key": "profile:user:preference:ramen", "signal_queries": ["dinner ideas", "casual food", "dinner ideas"], "signal_confidence": 0.9, "min_signal_count": 3, "min_distinct_queries": 2, "min_confidence": 0.75, "expected_proposal_text_contains": ["ramen"], "forbidden_proposal_text_contains": ["do not save"]}}
{"id": "maintenance-threshold-blocks-single-query", "source_family": "maintenance", "difficulty": "medium", "generation_method": "seed", "setup_memories": [{"text": "The user prefers ramen for casual dinners.", "kind": "profile", "status": "active", "canonical_key": "profile:user:preference:ramen", "facet_tags": ["fact_about_user", "preference"], "entity_values": [], "topics": ["ramen"]}], "messages": [{"role": "user", "content": "Thanks, no memory update."}], "expected_write_count": 0, "expected_memories": [], "maintenance_expectation": {"signal_memory_canonical_key": "profile:user:preference:ramen", "signal_queries": ["dinner ideas", "dinner ideas"], "signal_confidence": 0.9, "min_signal_count": 3, "min_distinct_queries": 2, "min_confidence": 0.75, "expected_proposal_count": 0, "forbidden_proposal_text_contains": ["ramen"]}}
{"id":"presence-location-does-not-become-residence","case_category":"adversarial","source_family":"no_write","difficulty":"hard","generation_method":"seed","review_status":"curated","synthetic_status":"synthetic","messages":[{"role":"user","content":"I'm in Lisbon, any dinner recommendations?"}],"expected_write_count":0,"expected_memories":[],"recall_queries":[]}
{"id":"qualified-presence-location-does-not-become-residence","case_category":"known_regression","source_family":"no_write","difficulty":"hard","generation_method":"seed","review_status":"curated","synthetic_status":"synthetic","messages":[{"role":"user","content":"I'm in Tokyo currently for work."}],"expected_write_count":0,"expected_memories":[],"recall_queries":[]}
41 changes: 28 additions & 13 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,42 @@ import Foundation
import PackageDescription

// Some Apple Swift CLT/snapshot installs place Swift Testing in Developer
// frameworks instead of the default SwiftPM search paths.
let developerFrameworkSwiftSettings: [SwiftSetting] = [
"/Library/Developer/CommandLineTools/Library/Developer/Frameworks",
"/Applications/Xcode.app/Contents/Developer/Library/Developer/Frameworks",
].compactMap { path in
// frameworks instead of the default SwiftPM search paths. When DEVELOPER_DIR
// is set, use only that toolchain's paths: mixing another Xcode's Testing
// framework with the selected compiler's @Test macro fails to build.
let developerDir = ProcessInfo.processInfo.environment["DEVELOPER_DIR"]

let developerFrameworkPaths: [String] = {
if let developerDir, !developerDir.isEmpty {
return [developerDir + "/Library/Developer/Frameworks"]
}
return [
"/Library/Developer/CommandLineTools/Library/Developer/Frameworks",
"/Applications/Xcode.app/Contents/Developer/Library/Developer/Frameworks",
]
}()

let developerLibraryPaths: [String] = {
if let developerDir, !developerDir.isEmpty {
return [developerDir + "/Library/Developer/usr/lib"]
}
return [
"/Library/Developer/CommandLineTools/Library/Developer/usr/lib",
"/Applications/Xcode.app/Contents/Developer/Library/Developer/usr/lib",
]
}()

let developerFrameworkSwiftSettings: [SwiftSetting] = developerFrameworkPaths.compactMap { path in
guard FileManager.default.fileExists(atPath: path) else { return nil }
return .unsafeFlags(["-F", path], .when(platforms: [.macOS]))
}

let developerFrameworkLinkerSettings: [LinkerSetting] = [
"/Library/Developer/CommandLineTools/Library/Developer/Frameworks",
"/Applications/Xcode.app/Contents/Developer/Library/Developer/Frameworks",
].compactMap { path in
let developerFrameworkLinkerSettings: [LinkerSetting] = developerFrameworkPaths.compactMap { path in
guard FileManager.default.fileExists(atPath: path) else { return nil }
return .unsafeFlags(["-F", path, "-Xlinker", "-rpath", "-Xlinker", path], .when(platforms: [.macOS]))
}

let developerLibraryLinkerSettings: [LinkerSetting] = [
"/Library/Developer/CommandLineTools/Library/Developer/usr/lib",
"/Applications/Xcode.app/Contents/Developer/Library/Developer/usr/lib",
].compactMap { path in
let developerLibraryLinkerSettings: [LinkerSetting] = developerLibraryPaths.compactMap { path in
guard FileManager.default.fileExists(atPath: path) else { return nil }
return .unsafeFlags(["-L", path, "-Xlinker", "-rpath", "-Xlinker", path], .when(platforms: [.macOS]))
}
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Most integrations only need:
- `save`, `extract`, `ingest`, and `recall` for agent memory workflows
- `capture`, `prepareContext`, `recordSignal`, and `runMaintenance` for higher-level agent memory workflows
- `memorySearch` and `memoryGet` for tool-style retrieval
- customization protocols (`EmbeddingProvider`, `Reranker`, `StructuredQueryExpander`, `MemoryExtractor`, `RecallPlanner`) only when you are swapping in your own providers
- customization protocols (`EmbeddingProvider`, `Reranker`, `StructuredQueryExpander`, `MemoryExtractor`, `RecallPlanner`, `EntityTagger`) only when you are swapping in your own providers

## Agent Memory Workflows

Expand All @@ -131,6 +131,12 @@ let capture = try await index.capture(

Captured `MemoryCandidate` and stored `MemoryRecord` values can include a `subject` and original-message `evidence`. The default agent policy focuses on user-authored durable facts, rejects assistant capability/refusal text, and keeps embedded declarations separate from raw questions.

### Linguistic Entity Tagging

When the `MemoryNaturalLanguage` trait is enabled (the default), every extraction provider is followed by a shared entity-enrichment stage using `NLEntityTagger`, an `NLTagger`-backed named entity recognizer. Extractor-supplied and heuristic entities remain the floor. Linguistic annotations can conservatively specialize generic labels or add missed entities when the surrounding prose supports the proposed person/location/organization label, but they do not independently create candidates, facets, or durable write decisions. NaturalLanguage does not expose calibrated confidence for name tags, so its annotations are intentionally treated as advisory.

Residence extraction requires an explicit durable relation such as "I live in", "I'm based in", or "I moved to". Presence statements such as "I'm in Portland" may describe travel and are not rewritten as residence even when Portland is recognized as a place. Set `entityTagger: nil` to disable linguistic enrichment, or provide a custom `EntityTagger`; high-confidence custom annotations can be accepted without the default contextual corroboration.

Context preparation retrieves bounded memory context for the next model turn:

```swift
Expand Down
Loading
Loading