Skip to content

Releases: gregyuzik/AskKit

AskKit 0.3.1

Choose a tag to compare

@gregyuzik gregyuzik released this 29 Oct 22:23
Immutable release. Only release title and notes can be modified.
v0.3.1
af36a07

The composer's hint no longer makes an empty field two lines tall.

Changed

  • The composer's hint takes one line, ending in an ellipsis when it is longer than the field is wide,
    so an empty field is always one line tall; only a draft grows the capsule. A hint that wrapped held
    the capsule two lines tall on an iPhone before the reader had typed a word. VoiceOver still reads the
    hint whole. Keep a host's hint short enough to show whole.

AskKit 0.3.0

Choose a tag to compare

@gregyuzik gregyuzik released this 29 Oct 18:01
Immutable release. Only release title and notes can be modified.
v0.3.0
2228e61

The public API's shape, and fixes across the engine, the stored chats and the screen, many of them for
VoiceOver and the accessibility sizes, and for a reader on gaze, a pointer or a hardware keyboard.

This version breaks the public API, on purpose. Each value has one type (question kinds, lengths of
time, payloads), a value can no longer say two things at once, names say what they are, the enums that
grow are open, and what only AskKit can drive safely, the stored chats' reads and writes and the
generation gate, is no longer a host's to call. "Updating from 0.2" is the list a host works through.
Three more changes a host sees: AskDomain.perform(_:) runs off the main actor, the confirmation
card's Confirm and the nothing-to-ask button are black on the system blue and red, and AskCommands
puts the newest turn's Copy, Copy answer, Edit & Resend and Ask again in the Mac's File menu.

Updating from 0.2

Work down the list, building as you go: the compiler finds all but the last step for you.

  1. Names. AskAsking is AskAnswering, and its ask requirements take mode: where they took
    origin:. engine.ask(q, origin: …) is engine.ask(q, mode: …): .conversation, the default,
    where you passed .typed, .starter, .followUp or .prefilled, and .reroll or .standalone
    as before. AskTurn.Failure.retryable is isRetryable. AskTurnStats' msToFirstTool,
    msToFirstWord, msToLastWord and msTotal are timeToFirstTool, timeToFirstWord,
    timeToLastWord and duration, and AskOutcome.milliseconds is duration.
  2. @unknown default. End every switch of yours over an open enum, AskError, AskTier and
    AskTurnEvent among them (README.md's "Switching over AskKit's enums" lists them), with
    @unknown default.
  3. Typed kinds and lengths of time. questionKinds is [AskQuestionKind] and rerollableKinds
    Set<AskQuestionKind>, and a kind is a static member: AskScenario(expectedKinds: [.count]),
    AskAnswer(questionKind: .pick). A length of time is a Duration: AskDeadline.run(.seconds(5))
    where you wrote run(seconds: 5).
  4. Typed payloads, and try. Drop your JSONEncoder and hand AskKit the value:
    try collector.record(payload: value), try AskProposal(…, payload: value),
    try AskAnswer(…, payload: value) and try answer.replacing(payload: value).
    mergePayload(as:_:) takes try too, and deterministicAnswer(for:) is async throws, so write
    try there, not try?.
  5. The first screen. Delete firstScreenPolicy and implementsDeterministicAnswer, and set
    answersWithoutModel to true where either said your domain answers without a model. Keep
    nothingToAskCopy only if an empty overview should show the nothing-to-ask screen: its presence is
    the policy. Move the button's label out of AskBrand(nothingToAskActionLabel:) and its closure out
    of AskScreen(onNothingAction:), into one
    AskScreen(store: store, nothingToAskAction: AskNothingToAskAction("Add Your First Item") { … }).
  6. The store, the evaluation and the seam. Build the store with AskConversationStore(engine:)
    and the evaluation with AskEval(domain:seam:log:). Delete every gate: argument and any
    AskGenerationGate you built: your own on-device generations go through AskGenerationGate.run.
    Call AskThreadStore, AskThreadArchive and AskFeedbackAttachments for nothing but their
    initializers and caps, and list the chats with the store's history.
  7. States that can't contradict themselves. Build AskTierState with unavailability: for a down
    tier, and no isAvailable:. Switch over AskRetryAfter (.rateLimited(until:),
    .quotaReached(resetDate:)), read outcome.failure?.isUngrounded, unwrap
    AskPrivateCloudState.of(_:). A showsDiagnostics: AskDiagnostics.isEnabled can go: nil, the
    card's default now, follows the switch as the card draws.
  8. What else went. Read AskDiagnostics.isTestFlight on the main actor, and import AskUI where
    you name AskOrigin. Drop AskBrand(shortLabel:), AskStarter(bank:) and
    AskFeedbackAttachments.removeAll(), and store AskPersistence and AskGroundingPolicy by
    rawValue, since neither is Codable.
  9. What runs differently, with no compile error: store.ask(q, origin: .reroll) runs as Another
    pick, an empty overview with no nothingToAskCopy shows the ready screen, and a
    deterministicAnswer(for:) that throws fails the turn.

Added

  • AskError.requestNotSupported, for a request the model answering can't take: a capability it
    lacks (tool calling, guided generation, reasoning), a generation guide it doesn't support, or prompt
    content it can't process. These were LanguageModelError's three unsupported… cases, which fell
    to the retryable .model(tag:). It offers no Try Again and no coaching, is never handed down to the
    other tier, and reads "Couldn't answer that. The model doesn't support what this app asks of it." in
    all 25 languages. Where the photo is what the model can't take (unsupportedCapability for
    vision, or unsupported content that carries the picture), the turn fails with
    .photosNotSupported.
  • AskConversationStore(engine:), which reads the engine's domain (AskEngine.domain, now
    public), so the store can't draw, keep and perform by a second copy of the domain that differs from
    the one the engine answers by. AskConversationStore(domain:engine:) is for an AskAnswering
    engine of a host's own, which carries no domain.
  • AskPrompt.fenceTags and AskEngine.restartFloor are public, read-only: the tag names a tool's
    output must not use, and the 10 s of its deadline a turn that has drawn prose needs left to be asked
    again from the start (house rule 5).
  • AppleAskSession builds on watchOS, so a host's own router can wrap its sessions in it there.
    AppleModelSeam is still compiled out on watchOS.

Changed

  • The public enums that grow are open (@nonexhaustive): AskError, AskUnavailability,
    AskTier, AskActivity, AskTurnEvent, AskReasoningLevel, AskGrounding, AskDiscard.Reason,
    AskRetryAfter, AskProposalKind, AskProposalOutcome, AskOutcome.Miss, AskOrigin,
    AskStage.Phase, AskProposalPhase and AskPrivateCloudState. A host's switch over one needs
    @unknown default, or it doesn't compile, and a case a later version adds lands there instead of
    breaking the build. The closed ones, AskTurnMode, AskOutcome.Failure, AskGroundingPolicy,
    AskReplyShape, AskEvidencePresentation, AskPersistence, AskPrivateCloudCompute,
    AskFeedback.Sentiment, AskChipRow.Kind and AskArrival.Kind, are still switched over case by
    case.
  • Question kinds are a type, AskQuestionKind, so a domain can't misspell one. A kind is a static
    member (.count, .pick, …), not a String: a typo such as "cuont" made the model's correct
    count read as other, and show tiles. AskDomain.questionKinds is [AskQuestionKind] and
    rerollableKinds Set<AskQuestionKind>, and AskAnswer, AskFeedback, AskScenario,
    AskOutcome and AskOutcome.Miss carry kinds too. AskQuestionKind(rawValue:) never fails, and a
    kind is stored as its bare word, so saved chats read back unchanged. The model is offered the same
    eleven words, and the [feedback] line and the evaluation's table print them as before.
  • The engine runs a turn in a mode, and the store keeps where its question came from.
    AskEngine.ask takes mode: AskTurnMode (.conversation, the default; .reroll, with Another
    pick's steer at temperature 0.8; .standalone, a one-shot that neither reads nor replaces the kept
    conversation) in place of origin:. AskOrigin moves from AskCore to AskUI with the same name, and
    says only where a question came from: .typed, .starter, .followUp, .prefilled or .reroll,
    with no .standalone, raw value or Codable. AskTurn.origin, AskTurn's initializer and
    AskConversationStore.ask(_:origin:) take AskUI's, and a target that imports AskCore alone no
    longer has the type.
  • The store asks every origin as itself. store.ask(q, origin: .reroll) runs as Another pick,
    hotter and steered away from the last answer's picks, where it was asked as a typed question, and
    leaves the composer's draft, photo and scope where they are.
  • AskAsking is AskAnswering, since the conformer answers and the store asks, and its ask
    requirements take mode: AskTurnMode. AskTurn.Failure.retryable is isRetryable, its
    initializer's label included, as AskError.isRetryable already says it; a thread file's
    failureIsRetryable key is unchanged.
  • An evaluation builds its own engine: AskEval(domain:seam:log:), in place of
    AskEval(engine:). Handed the thread's engine, the evaluation reset it before its first scenario,
    dropping the reader's conversation and warm session and leaving the store's hasKeptConversation
    stale. It can share the thread's domain and seam.
  • A payload is your own value, which AskKit encodes as JSON. AskCollector.record(payload:),
    AskProposal(…, payload:), AskAnswer(…, payload:) and AskAnswer.replacing(payload:) take your
    value (some Encodable & Sendable, or some Encodable for a proposal), where they took Data, and
    throw the encoder's EncodingError for a value that doesn't encode, such as a NaN, and for a
    Data, which is a payload already encoded. An oversize payload still makes record(payload:)
    return false, which a tool can tell the model. mergePayload(as:_:) throws for a result that
    doesn't encode, where it returned false. A proposal or an answer with no payload takes the
    initializer without one, which doesn't throw. Payloads in chat files 0.2.0 wrote still read back.
  • AskDomain.deterministicAnswer(for:) is async throws. A throw fails the turn and asks no
    model: an AskError ...
Read more

AskKit 0.2.0

Choose a tag to compare

@gregyuzik gregyuzik released this 28 Oct 13:50
Immutable release. Only release title and notes can be modified.
v0.2.0
b0d6931

A privacy manifest for AskCore, and a plainer composer that no longer needs Xcode's Metal Toolchain to
build. The public API is unchanged.

Added

  • A privacy manifest for AskCore. AskCore's resource bundle carries a PrivacyInfo.xcprivacy, so
    an app that embeds AskKit gets a complete privacy report. It declares the two required-reason APIs
    AskCore calls: user defaults for CA92.1, read only for the launch's own arguments (ASK_LIVE_PCC),
    and the file timestamp for C617.1, the modification date of a chat file in the app's own container.
    It declares no tracking and no collected data: AskKit sends nothing to its developer or any third
    party. AskUI calls none of these APIs, and carries no manifest.

Removed

  • The Glass Rod round the composer. The spectrum look's clear glass tube of colors round the
    composer's rim, there at rest and drifting while a turn was in flight, was too distracting, and the
    composer was better without it: it is plain glass again, in the accent's tint. A spectrum tints the
    send circle and the streaming caret, on the slot's face too, and nothing else. With the rod went
    AskUI's only Metal shader, so building AskKit no longer needs Xcode's Metal Toolchain.

AskKit 0.1.0

Choose a tag to compare

@gregyuzik gregyuzik released this 27 Oct 21:31

The first public release: a complete Ask screen over Apple's Foundation Models, for any kind of app. A
reader asks in their own words and gets a short answer, from the app's data, through the tools the app
gives the model, or from the model alone where the app allows it. The app implements one protocol,
AskDomain, and AskKit supplies the engine (AskCore) and the SwiftUI screen (AskUI).

It needs Xcode 27.2 or later on a Mac with Apple silicon, Swift 6.4, and Xcode's Metal Toolchain
component (xcodebuild -downloadComponent MetalToolchain), since AskUI compiles a Metal shader. Every
platform's deployment target is 27.2. The on-device model needs Apple Intelligence on the reader's
device, and Private Cloud Compute, which is optional, needs Apple's managed entitlement.

Added

  • AskCore, the engine, with no UI. A domain gives it a brand, tools and instructions; one over the
    app's own rows adds its row type, its search's arguments, an overview and a search, and one whose
    tools read no rows leaves those to their defaults.
    • Private Cloud Compute answers when it can serve and the on-device model when it can't, behind one
      model seam (AskModelSeam, which AppleModelSeam implements). When Private Cloud Compute goes out
      mid-turn, the turn is handed down once to the on-device model. The seam sends Private Cloud Compute
      nothing unless the host declares the entitlement, and keeps a development build off it unless the
      run opts in (ASK_LIVE_PCC=1), so the developer's own daily quota isn't spent.
    • Grounding is the domain's policy (AskGroundingPolicy). .retrieval, the default, keeps only
      answers grounded in the app's data, and when a first answer read nothing it runs the app's own
      search and asks again (the grounded retry). .toolCall keeps answers a tool call grounded, and
      .notRequired keeps every answer. Each answer records what grounded it (AskAnswer.grounding).
    • Actions the model proposes (AskProposal), which the reader confirms on a card before the domain's
      perform(_:) carries them out, or which open the app's own editor.
    • The line under an answer is the domain's (AskDomain.evidence(for:)): AskKit's count of the rows
      it read, the domain's own words, or no line. An answer nothing grounded says it isn't from the
      app's data.
    • A reply with no tiles, for a domain whose answers never show any (AskReplyShape.prose): on the
      device its schema costs 192 tokens of the window where the full reply's costs 704.
    • A conversation that carries from turn to turn, one deadline over the whole turn (the domain's
      hooks, every generation, the grounded retry and the hand-down), and every failure triaged into an
      AskError with a line for the reader.
    • Tool calls through AskToolContext.withToolCall(_:_:), fields through AskPrompt.clamp, and what
      AskKit composes fenced, with its fence tags redacted in any spelling that compatibility
      decomposition (NFKD) maps to the tag once marks are dropped.
    • AskLink, a deep link that prefills the composer and never sends; AskEngine.answerOnce(_:), one
      answer outside the thread, for a Siri intent; and AskEval, which runs scenarios through a real
      engine.
  • AskUI, the Ask screen for iOS, macOS and visionOS, and the AskConversationStore that drives
    it, which keeps the thread and the earlier chats on disk, where no failed write or interrupted change
    drops a chat.
    • The first screen with the app's starters; the thread, with the wait row, the prose as it streams,
      the app's tiles, the evidence line, the confirmation card and follow-up chips; chat history; the
      composer in the tab bar slot on iOS; menu commands on the Mac (AskCommands); and
      AskPrivateCloudCard for the app's settings.
    • A host sets its own views with modifiers on AskScreen(store:), and opens a tile's row or a
      hand-off's editor through a class of its own conformed to AskItemRouter or AskDraftRouter.
    • The spectrum look, opt-in: AskScreen(store:spectrum:) takes an AskSpectrum, such as
      .aurora. Its colors light the Glass Rod, a clear glass tube round the composer's rim drawn by a
      Metal shader, there at rest and drifting while a turn is in flight, and the send circle and the
      streaming caret take a gradient of them. Increase Contrast puts the accent back and drops the rod,
      and Reduce Motion holds its colors still.
    • On iPhone Duo's inner display the screen arranges itself around the fold.
  • Strings in 25 languages, in two String Catalogs, with a check that fails a missing locale, plural
    or placeholder.
  • Five platforms. Both libraries compile for iOS, macOS, visionOS, tvOS and watchOS, so a host's
    shared package can link them on every platform it ships. The screen shows on iOS, macOS and visionOS.
  • A demo app for iOS, macOS and visionOS: a library of thirty items behind one search tool, or,
    with -askAssistant, an assistant with no rows of its own that converts units and proposes
    reminders. A launch argument sets up each state of the screen, and -askScriptedAnswer answers
    without a model.