← Wszystkie wpisy

Prawdziwym kosztem SwiftData jest dyscyplina schematu

ShoppingItem z aplikacji Get Bananas to kanoniczny przykład tego, dlaczego dyscyplina schematu w SwiftData ma znaczenie. Pierwotny schemat nie zawierał znacznika czasu lastModified; dodanie go później wymagało określonego kształtu migracji, ponieważ istniejące dane były już zapisane na dysku, a pole zostało zadeklarowane jako opcjonalne właśnie po to, by naprawić awarię migracji, która wystąpiła przy pierwszym dodaniu go jako nieopcjonalnego.1

API SwiftData to dwa makra. @Model na klasie czyni z niej typ trwały. @Attribute(.unique) na właściwości nadaje jej ograniczenie unikalności. Framework ukrywa zarządzanie stosem Core Data, taniec z transformatorami wartości oraz boilerplate NSManagedObjectContext. To, czego framework nie ukrywa, to migracja schematu; po prostu czyni ją deklaratywną zamiast imperatywną. Kosztem nieuwagi przy migracjach jest błąd, który wymazuje dane użytkownika podczas rutynowej aktualizacji.

Teza: SwiftData jest tani na początku i drogi przy niedbałych migracjach. Dyscyplina to nazewnictwo, opcjonalność i VersionedSchema od pierwszego dnia, a nie od dnia, w którym uświadomi się Pan/Pani, że powinno się je było wprowadzić.

TL;DR

  • Makro @Model zamienia klasę w trwały typ SwiftData. Framework generuje schemat z deklaracji właściwości w czasie kompilacji.
  • Dodanie nowej właściwości opcjonalnej to migracja bez zmian: lekka migracja SwiftData obsługuje to automatycznie. Dodanie właściwości nieopcjonalnej do istniejącego schematu wymaga VersionedSchema oraz MigrationPlan, który mówi frameworkowi, jak wypełnić nowe pole dla istniejących wierszy.
  • Kosztem pominięcia VersionedSchema od pierwszego dnia jest to, że każda nietrywialna zmiana schematu w v2 ryzykuje porzuceniem bazy danych użytkownika, ponieważ ścieżka lekkiej migracji jest zachowawcza i przerywa, gdy nie potrafi wywnioskować migracji.
  • @Attribute(.unique) to właściwe narzędzie dla kluczy naturalnych (wygenerowanego UUID, zaimportowanego identyfikatora zewnętrznego). @Relationship to właściwe narzędzie dla referencji rodzic/dziecko. Oba są makrami, które generują pod spodem właściwą instalację Core Data.2

Co tak naprawdę robi @Model

Typ SwiftData to klasa Swift z zastosowanym makrem @Model. Kanoniczny kształt z Get Bananas to ShoppingItem:

import Foundation
import SwiftData

@Model
final class ShoppingItem {
    @Attribute(.unique) var id: UUID
    var name: String
    var amount: String
    var section: String
    var isChecked: Bool
    var isOptional: Bool
    var sortOrder: Int
    var lastModified: Date?

    init(id: UUID = UUID(), name: String, amount: String, section: String,
         isOptional: Bool = false, sortOrder: Int = 0) {
        self.id = id
        self.name = name
        self.amount = amount
        self.section = section
        self.isChecked = false
        self.isOptional = isOptional
        self.sortOrder = sortOrder
        self.lastModified = Date()
    }
}

Trzy szczegóły dotyczące tego kształtu, które API ukrywa.

@Model nie wymaga osobnej deklaracji schematu trwałego magazynu. SwiftData odczytuje definicję klasy w czasie kompilacji i syntetyzuje schemat. Właściwości klasy stają się atrybutami modelu; ich typy Swift stają się typami kolumn. Nie ma pliku .xcdatamodeld do utrzymania (choć podstawowy NSManagedObjectModel z Core Data nadal istnieje i jest tym, co stanowi zaplecze schematu w czasie wykonania).2

@Attribute(.unique) to ograniczenie na pojedynczej kolumnie, a nie deklaracja PRIMARY KEY. Trwałą tożsamością SwiftData jest PersistentIdentifier, generowany automatycznie dla każdego wiersza. Deklaracja @Attribute(.unique) mówi frameworkowi: „ta kolumna przechowuje co najwyżej jeden wiersz na każdą wartość”. Gdy wstawia się model z wartością .unique, która już istnieje, SwiftData wykonuje upsert: istniejący wiersz jest aktualizowany, a nie odrzucany. Te semantyki mają znaczenie dla kodu produktowego: .unique nie jest walidacją na poziomie UI, która zapobiega zgłaszaniu duplikatów; jest gwarancją „co najwyżej jeden” w przechowywaniu, która po cichu scala. Wzorzec id: UUID powyżej jest tym zalecanym dla synchronizacji międzyprocesowej (gdzie chce się stabilnego identyfikatora przeżywającego znikający w procesie PersistentIdentifier), a zachowanie upsert to dokładnie to, czego się chce, gdy ten sam UUID dociera z dwóch ścieżek synchronizacji.

Klasy @Model są typami referencyjnymi, nie wartościowymi. Mutowanie właściwości na instancji ShoppingItem wyzwala śledzenie zmian SwiftData; framework rejestruje zmianę i utrwala ją przy następnym zapisie kontekstu. Integracja z SwiftUI poprzez @Query ponownie renderuje każdy widok obserwujący pasujący predykat. Wzorzec jest podobny do @Observable (omówionego w Z czego zbudowany jest SwiftUI), z dodaną na wierzchu warstwą trwałości.

Pola opcjonalne to tania migracja

Pole lastModified: Date? na ShoppingItem jest opcjonalne, a ta opcjonalność jest nośna. Pole zostało dodane po wydaniu v1, aby wspierać synchronizację między urządzeniami i rozwiązywanie konfliktów; istniejące wiersze na urządzeniach użytkowników nie miały żadnej wartości lastModified. Pole opcjonalne bez wartości domyślnej pozwala lekkiej migracji SwiftData obsłużyć to dodanie bez pisania jakiegokolwiek kodu migracyjnego: istniejące wiersze otrzymują nil; nowe wiersze otrzymują to, co ustawia init.3

Ścieżka lekkiej migracji to uprzejma ścieżka frameworka. SwiftData inspekcjonuje nowy schemat i trwały magazyn, wnioskuje najmniejszą zgodną zmianę i ją stosuje. Migracja jest automatyczna; użytkownik nic nie widzi; aplikacja uruchamia się normalnie na istniejących danych. Przypadki, które ścieżka lekka obsługuje czysto:

  • Dodanie właściwości opcjonalnej
  • Usunięcie właściwości (dane są porzucane; istniejące odczyty nie widzą już kolumny)
  • Zmiana nazwy atrybutu, którą framework potrafi dopasować po wskazówce (przy użyciu @Attribute(originalName: ...))
  • Zmiana nazwy klasy @Model, którą framework potrafi dopasować (przy użyciu @Model.originalName lub wskazówki)

Przypadki, w których ścieżka lekka przerywa:

  • Dodanie właściwości nieopcjonalnej bez wartości domyślnej do istniejącego schematu (istniejące wiersze nie mają wartości do wypełnienia)
  • Zmiana typu właściwości (np. IntString)
  • Podział modelu na dwa modele lub scalenie dwóch w jeden
  • Cokolwiek, co wymaga niestandardowej logiki do migracji

Gdy ścieżka lekka przerywa, bezpiecznym zachowaniem jest niepowodzenie migracji. Niebezpiecznym zachowaniem byłoby porzucenie bazy danych i zaczęcie od nowa; framework jest zachowawczy i odmawia robienia tego po cichu. Użytkownik widzi awarię aplikacji przy uruchomieniu z błędem migracji; deweloper widzi ślad stosu wskazujący na niezgodność schematu; nikt nie traci danych, ale wszyscy tracą zaufanie.

Koszt pominięcia VersionedSchema od pierwszego dnia ujawnia się na granicy v2 → v3, gdy dodaje się trzecią funkcję, której zmiana schematu przekracza to, co obsługuje ścieżka lekka.

VersionedSchema i MigrationPlan: dyscyplina od pierwszego dnia

VersionedSchema deklaruje konkretną wersję schematu modelu. MigrationPlan deklaruje, jak migrować z jednej wersji do następnej.4 Kształt:

import SwiftData

enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] = [ShoppingItemV1.self]
}

enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] = [ShoppingItemV2.self]
}

enum AppMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] = [
        SchemaV1.self,
        SchemaV2.self,
    ]

    static var stages: [MigrationStage] = [
        MigrationStage.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
    ]
}

Same klasy modelowe przenoszą się do przestrzeni nazw schematu wersjonowanego:

extension SchemaV1 {
    @Model
    final class ShoppingItemV1 { /* v1 fields */ }
}

extension SchemaV2 {
    @Model
    final class ShoppingItemV2 { /* v2 fields, including lastModified */ }
}

ModelContainer jest konstruowany z planem migracji:

let container = try ModelContainer(
    for: ShoppingItemV2.self,
    migrationPlan: AppMigrationPlan.self,
    configurations: ModelConfiguration("ShoppingList")
)

Plan migracji daje frameworkowi typowany graf tego, jak schemat ewoluuje. Gdy aplikacja wydająca v2 uruchamia się przeciwko bazie danych v1, framework przechodzi po planie migracji, stosuje nazwane etapy i sprowadza bazę danych do v2. Gdy wydaje się v3, dodaje się SchemaV3.self do schemas i nowy MigrationStage między v2 a v3.

Dyscyplina polega na wydaniu VersionedSchema w v1, nawet gdy istnieje tylko jedna wersja. Kosztem zrobienia tego jest jeden dodatkowy plik i jedna dodatkowa deklaracja enum. Kosztem nierobienia tego jest to, że pierwsza nietrywialna zmiana schematu w v2 wymaga retroaktywnego owinięcia v1 w VersionedSchema, co jest wykonalne, ale wymaga staranności, aby dopasować dokładny kształt v1, by framework mógł zidentyfikować istniejące dane jako SchemaV1. Przyszły Pan/Pani pracujący nad v2 zapłaci ten podatek; obecny Pan/Pani może zapłacić go raz i o nim zapomnieć.

Niestandardowy MigrationStage dla trudnych przypadków

Lekkie migracje pokrywają większość zmian addytywnych. Zmiany typu, podziały, scalenia i populacje warunkowe potrzebują MigrationStage.custom:

static var stages: [MigrationStage] = [
    MigrationStage.custom(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self,
        willMigrate: { context in
            // Read v1 rows; stage any derived state to a transient store
            // (UserDefaults / temp file) since the v1 and v2 contexts do
            // not share state, and didMigrate cannot read v1.
            let v1Items = try context.fetch(FetchDescriptor<ShoppingItemV1>())
            stageDerivedState(from: v1Items)
        },
        didMigrate: { context in
            // Populate v2-only fields on existing rows
            let v2Items = try context.fetch(FetchDescriptor<ShoppingItemV2>())
            for item in v2Items where item.lastModified == nil {
                item.lastModified = Date()
            }
            try context.save()
        }
    )
]

Te dwie domknięcia uruchamiają się przed i po zastosowaniu przez framework migracji strukturalnej. willMigrate działa względem schematu v1; didMigrate działa względem schematu v2. Ciało domknięcia to normalny kod SwiftData (deskryptory pobrania, zapisy kontekstu modelu, te same APIy używane w działającej aplikacji), operujący względem przejściowego kontekstu w trakcie migracji.

Wzorzec, który przeżywa w produkcji, polega na utrzymywaniu willMigrate pustego i umieszczeniu całej logiki populacji w didMigrate. Odczyt danych v1 wewnątrz willMigrate jest dozwolony, ale schemat v2 nie istnieje jeszcze z perspektywy frameworka, więc każde obliczenie musi być umieszczone w przejściowym magazynie, który domknięcie didMigrate może odczytać. Prostsza zasada: migracje strukturalne to robota frameworka; populowanie pól tylko-v2 na istniejących wierszach to robota didMigrate.

Kiedy @Attribute i @Relationship zarabiają na swoje nazwy

Dwa makra wykonują większość pracy dekoracyjnej schematu w klasach @Model.

@Attribute dekoruje pojedynczą właściwość ograniczeniem lub wskazówką:

  • @Attribute(.unique) wymusza unikalność, jak w ShoppingItem.id
  • @Attribute(.externalStorage) przechowuje duże bloby Data poza bazą danych (dane obrazów, bufory audio)
  • @Attribute(originalName: "old_field_name") dopasowuje właściwość do przemianowanej kolumny podczas migracji
  • @Attribute(.transformable(by: ...)) stosuje ValueTransformer do typu nie-Codable

Właściwa dyscyplina: używać .unique dla pól, które rzeczywiście powinny być unikalne (wygenerowane UUID, zewnętrzny identyfikator), używać .externalStorage dla każdego bloba większego niż kilka KB, używać originalName, gdy zmiana nazwy właściwości w v2 w przeciwnym razie utraciłaby dane v1.

@Relationship dekoruje właściwość wskazującą na inną klasę @Model lub ich kolekcję:

@Model
final class List {
    var name: String

    @Relationship(deleteRule: .cascade, inverse: \ShoppingItem.list)
    var items: [ShoppingItem] = []
}

@Model
final class ShoppingItem {
    var name: String
    var list: List?
}

deleteRule: .cascade oznacza, że usunięcie nadrzędnej List usuwa wszystkie podrzędne wiersze ShoppingItem. Parametr inverse: mówi frameworkowi, która właściwość na dziecku wskazuje z powrotem na rodzica; framework używa tego do przewidywalnej dwukierunkowej obsługi. SwiftData może czasem wnioskować inwersję automatycznie, a inverse: nil jest wspierane dla jawnie jednokierunkowych relacji, ale bezpiecznym domyślem jest deklarowanie inverse:, ilekroć wnioskowanie byłoby niejednoznaczne.5

Właściwa dyscyplina: deklarować relacje z jawnym deleteRule (domyślną wartością jest .nullify, co rzadko jest tym, czego się chce) i deklarować inverse:, ilekroć relacja jest dwukierunkowa (zamiast polegać na wnioskowaniu frameworka). Niejawne wartości domyślne są zwykle złe; jawna forma to jeden dodatkowy parametr i na zawsze zaoszczędzony błąd.

Co zbudowałbym inaczej

Trzy wzorce, które aplikacje w klastrze albo wydają, albo żałują, że ich nie wydały.

Wydawać VersionedSchema od v1. Każda wydawana klasa @Model powinna żyć wewnątrz VersionedSchema od pierwszego dnia. Kosztem jest jedno opakowujące enum na wersję schematu. Korzyścią jest to, że pierwsza nietrywialna zmiana w v2 jest jednowierszowym dodaniem do MigrationPlan.schemas, a nie dwudniowym retroaktywnym refaktoringiem.

Każdy znacznik czasu uczynić opcjonalnym. Pola takie jak lastModified, createdAt i updatedAt, które istnieją dla synchronizacji między urządzeniami lub rozwiązywania konfliktów, powinny być opcjonalne w v1, jeśli produkt v1 ich nie potrzebuje. Opcjonalność utrzymuje migrację do v2 (gdy się ich potrzebuje) tanią. Wypełnianie ich na istniejących wierszach podczas didMigrate to jedna pętla; uczynienie ich nieopcjonalnymi od v1 to ograniczenie, które może zepsuć backfill na danych użytkownika.

Używać UUID jako klucza naturalnego, a nie PersistentIdentifier. PersistentIdentifier SwiftData jest w obrębie procesu. Synchronizacja między urządzeniami, integracja MCP (omówiona w Dwa ekosystemy agentów, jedna lista zakupów) i każda referencja poza procesem potrzebują stabilnego identyfikatora. UUID z @Attribute(.unique) to właściwy kształt; w-procesowy PersistentIdentifier to zły kształt dla wszystkiego, co przekracza granicę procesu.

Kiedy @Model jest złą odpowiedzią

Trzy przypadki, w których SwiftData nie jest właściwym narzędziem:

Stan klucz/wartość pojedynczego rekordu. Ustawienia aplikacji, wybrany przez użytkownika język, znacznik czasu ostatniej synchronizacji. Należy używać UserDefaults lub NSUbiquitousKeyValueStore (omówione w Pięć platform Apple, trzy współdzielone pliki). Narzut SwiftData dla pojedynczego wiersza to zmarnowana ceremonia; magazyny klucz-wartość są właściwym substratem.

Dane autorytatywne po stronie serwera bez zapisów offline. Lista pobierana z REST API i wyświetlana tylko do odczytu. SwiftData jest przesadą, jeśli źródłem prawdy jest serwer, a lokalna pamięć podręczna jest tylko pamięcią podręczną. Prosty zrzut Codable w Documents/ plus tablica buforowana w pamięci wystarczy; podatek migracyjny SwiftData nie jest wart zapłacenia, jeśli dane nie przeżywają twardego resetu.

Koordynacja wieloprocesowa. SwiftData działa wewnątrz procesu. Serwer MCP działający poza aplikacją iOS nie może odczytywać ani zapisywać do kontenera SwiftData aplikacji. Stan międzyprocesowy potrzebuje innego kształtu: pliku JSON na iCloud Drive, współdzielonego kontenera App Group lub jawnej warstwy synchronizacji łączącej procesy. (Get Bananas paruje SwiftData z JSON na iCloud Drive właśnie z tego powodu.)6

Dane to duże bloby, które rzadko się zmieniają. Plik audio 10 MB, zestaw obrazów 50 MB. Należy używać @Attribute(.externalStorage), jeśli bloby są wewnątrz wierszy SwiftData; w przeciwnym razie używać systemu plików bezpośrednio z metadanymi w SwiftData wskazującymi na URL-e plików.

Co wzorzec oznacza dla aplikacji wydawanych na iOS 26+

Trzy wnioski.

  1. Makra to łatwa część. Migracje to koszt. @Model i @Attribute to dwuwierszowe deklaracje, które ukrywają sporo instalacji Core Data. Dyscyplina migracyjna to to, za co rzeczywiście się płaci przez cykl życia aplikacji; należy projektować v1 z myślą o v2.

  2. VersionedSchema od pierwszego dnia jest nienegocjowalna dla wydawanych aplikacji. Opakowujące enum to jeden dodatkowy plik. Retroaktywny koszt dodania go później jest znacznie wyższy.

  3. Pola opcjonalne i jawne relacje to tania polisa ubezpieczeniowa. Opcjonalne znaczniki czasu dla metadanych synchronizacji, jawne deleteRule i inverse: na relacjach. Oba to drobne deklaracje, które kupują dużo elastyczności w v2.

Pełny klaster Apple Ecosystem: typowane App Intents dla Apple Intelligence; serwery MCP dla agentów cross-LLM; pytanie o routing między nimi; Foundation Models dla LLM on-device i protokołu Tool; Live Activities dla maszyny stanów Lock Screen na iOS; kontrakt środowiska wykonawczego watchOS na Apple Watch; wnętrzności SwiftUI dla substratu frameworka; model mentalny przestrzeni RealityKit dla scen visionOS; wzorce Liquid Glass dla warstwy wizualnej; wieloplatformowe wydawanie dla zasięgu między urządzeniami. Hub znajduje się w serii Apple Ecosystem. Szerszy kontekst iOS-z-agentami-AI można znaleźć w przewodniku iOS Agent Development.

FAQ

Jaka jest różnica między @Model a NSManagedObject z Core Data?

@Model to makro Swift, które generuje pod spodem instalację NSManagedObject. SwiftData używa Core Data jako swojego magazynu zaplecza, więc model wykonawczy jest taki sam; różnica leży w warstwie powierzchniowej. @Model usuwa plik .xcdatamodeld, ceremonię transformatora wartości i zarządzanie cyklem życia NSManagedObjectContext. Otrzymuje się ten sam trwały magazyn z API ukształtowanym po Swiftowsku.

Czy potrzebuję VersionedSchema, jeśli nigdy nie planuję zmieniać schematu?

Jeśli aplikacja może wydać v2, tak. Jeśli to jednorazowe demo, nie. Kosztem VersionedSchema od v1 jest jedna dodatkowa deklaracja enum. Kosztem dodania go retroaktywnie w v2 jest dopasowanie dokładnego kształtu schematu v1, aby framework rozpoznał istniejące dane, co jest wykonalne, ale podatne na błędy. Większość wydawanych aplikacji w końcu będzie potrzebować zmiany schematu; warto budżetować na to w v1.

Kiedy powinienem/powinnam używać @Attribute(.unique)?

Gdy pole jest kluczem naturalnym dla wiersza: wygenerowanym UUID, zaimportowanym zewnętrznym identyfikatorem, przypisanym slugiem. SwiftData traktuje .unique jako upsert: jeśli wstawi się model, którego wartość .unique już istnieje, istniejący wiersz jest aktualizowany zamiast dołączania nowego wiersza. Te semantyki sprawiają, że ścieżki synchronizacji typu upsert (ten sam UUID przychodzący z dwóch urządzeń) są bezpieczne; to także powód, dla którego .unique to złe narzędzie na polach z nazwami wyświetlanymi, takich jak title, ponieważ dwóch użytkowników wpisujących ten sam tytuł po cichu scaliłoby swoje wiersze, zamiast wytworzyć dwa odrębne rekordy.

Jak obsłużyć pole nieopcjonalne dodane do istniejącego schematu?

Należy użyć MigrationStage.custom z domknięciem didMigrate, które wypełnia pole na istniejących wierszach. Albo, łatwiej: zadeklarować pole jako opcjonalne w nowej wersji schematu i wypełniać je leniwie przy dostępie. Opcjonalność to tańsza migracja; dodanie nieopcjonalnych wymaga jawnej logiki populacji.

Czym jest PersistentIdentifier w porównaniu z moim własnym UUID?

PersistentIdentifier to ID wiersza SwiftData w obrębie procesu; jest generowany automatycznie i przeżywa cykl życia działającego procesu. Własny UUID z @Attribute(.unique) jest stabilnym identyfikatorem międzyprocesowym i międzyurządzeniowym. Należy używać PersistentIdentifier dla referencji w obrębie procesu wewnątrz aplikacji. Należy używać UUID dla wszystkiego, co przekracza granicę procesu (synchronizacja między urządzeniami, integracje zewnętrzne, narzędzia MCP, wywołania sieciowe).

Bibliografia


  1. Autorska aplikacja Get Bananas, aplikacja listy zakupów w SwiftUI, która paruje SwiftData z synchronizacją JSON na iCloud Drive i serwerem MCP. Model ShoppingItem ewoluował przez wczesny cykl rozwojowy; pole lastModified: Date? zostało dodane po początkowym schemacie (commit 268a00d z 2025-12-01, „Make lastModified optional to fix migration crash”), ponieważ uczynienie go nieopcjonalnym psuło migrację, gdy istniejące wiersze nie miały wartości do wypełnienia. 

  2. Apple Developer, „SwiftData” i „Adding and editing persistent data in your app”. Makro @Model, powierzchnia ograniczeń @Attribute oraz relacja do NSManagedObjectModel z Core Data. 

  3. Apple Developer, „Preserving your app’s model data across launches” i „Adopting SwiftData for a Core Data app”. Semantyka lekkich migracji i to, co wyzwala framework do przerwania. 

  4. Apple Developer, „VersionedSchema” i „SchemaMigrationPlan”. Deklaracje schematów wersjonowanych, definicje etapów migracji oraz konstruktor ModelContainer przyjmujący plan migracji. 

  5. Apple Developer, „Defining data relationships with enumerations and model classes” i „Schema.Relationship”. Makro @Relationship, opcje deleteRule (.cascade, .nullify, .deny, .noAction) oraz rola parametru inverse: w utrzymaniu dwukierunkowej relacji. 

  6. Analiza autora w Dwa ekosystemy agentów, jedna lista zakupów, 29 kwietnia 2026, oraz Pięć platform Apple, trzy współdzielone pliki. Wzorce synchronizacji międzyprocesowej i międzyurządzeniowej Get Bananas + Return, które uzupełniają (a czasem zastępują) SwiftData wewnątrz wieloprocesowego workflow. 

Powiązane artykuły

Migracje SwiftData: lekkie kontra niestandardowe — i kiedy nie potrzeba V2

Model migracji SwiftData wykorzystuje VersionedSchema, MigrationStage i SchemaMigrationPlan. Większość zmian schematu ni…

10 min czytania

SwiftData w iOS 27: obserwacja i historia

iOS 27 daje SwiftData pełnoprawną obserwację zmian dzięki ResultsObserver, obserwację trwałej historii dzięki HistoryObs…

9 min czytania

Warstwa porządkowa to prawdziwy rynek agentów AI

Charlie Labs zmieniło kierunek z budowania agentów na sprzątanie po nich. Rynek agentów AI przesuwa się z generowania w …

11 min czytania