finnvoor / PiDurableKit
git clone https://github.com/finnvoor/PiDurableKit.git
Durable AI agents for Apple platforms: pi-durable on JavaScriptCore with a Swift API
—
stars, 24h
—
stars, 7 days
+272
stars, 30 days · #943
272
★ total
25
forks · 2 open issues
Star history starts 2026-10-20; the chart appears after the second daily snapshot.
LanguageSwift
LicenseCC0-1.0
Created2026-10-08 (12d ago)
Last push1d ago
Watchers1
OwnerUser
Links
README excerpt
# PiDurableKit
Durable AI agents for iPhone, iPad, Mac, and Vision Pro, powered by `@earendil-works/pi-durable` running in JavaScriptCore.
Conversations, model turns, tool calls, and your own state are committed to storage before anything is shown. If iOS kills your app mid-turn, reopening the storage picks the work up where it stopped.
[code]
- Swift tools, system prompt sections, hooks, and wraps, written as plain closures
- Subagents, durable tasks with child tasks, and atomic transactions
- Every pi-ai provider (Anthropic, OpenAI, Google, OpenRouter, Groq, xAI, Mistral, …) plus any OpenAI- or Anthropic-compatible server
- SQLite or JSONL persistence with crash recovery, or in-memory storage
- pi-durable's `read`/`write`/`edit` tools in a sandboxed directory
- Streaming: `AsyncSequence`s of conversation views (for SwiftUI) and coding-agent style events
- Steering, follow-ups, abort, reset, compaction, forks, typed documents and entries, usage and cost
- OAuth sign-in to Claude Pro/Max, ChatGPT, GitHub Copilot, and OpenRouter, with tokens in the Keychain and automatic refresh
- A scripted faux provider for tests and SwiftUI previews
Requires iOS 17, macOS 14, tvOS 17, or visionOS 1.
## Installation
[code]
and add `PiDurableKit` to your target's dependencies.
## Concepts
The Swift API mirrors pi-durable's, with Swift concurrency in place of Chord contexts (cancel the Swift task to cancel a wait; cancelling a wait never cancels the work).
| pi-durable | PiDurableKit |
|---|---|
| `createModels({ credentials })` + providers | `Models(credentials:)`, `models.register(CustomProvider(…))` |
| `Harness.open(storage, { models, registry, settings })` | `Harness.open(.sqlite(at:), models:, extensions:, settings:)` |
| `harness.root()`, `createConversation()`, `fork()` | `harness.root(agent:)`, `harness.createConversation(ownedBy:agent:)`, `conversation.fork(at:ownedBy:)` |
| `tx.scanConversations()`, `tx.conversation()`, `tx.entry()` | `harness.conversations(ownedBy:)`, `conversation.record()`, `conversation.entry(_:)` |
| `conversation.submit({ type: "input", … })` | `conversation.submit("…", whenBusy:, requestId:)` |
| `conversation.submit({ type: "write", … })` | `conversation.write(EntryDraft(…))` |
| `submission.wait()`, `tx.entry(AssistantEntry, id)` | `submission.wait()`, `tx.entry(id)?.assistantMessage` |
| `harness.submission(id)`, `abortSubmission(id, context, conversationId)` | `harness.submission(_:)`, `harness.abortSubmission(_:in:)` |
| `conversation.context({ at })` → `head`, `entries`, `contributions`, `messages` | `conversation.context(at:)` → `ContextView` |
| `EntryDraft.head`, `EntryDraft.edits` (`ContextEdit`) | `EntryDraft(kind:head: .itself / .entry(id), edits: [.omit(id), .replace(id, with:)])` |
| `conversation.configure(change)` | `conversation.configure(AgentChange(…))` |
| `defineExtension`, `defineTool`, `section`, `hook` | `Extension`, `Tool`, `PromptSection`, `Hook` |
| `wrapTool`, `wrapSection` | `Wrap.tool(_:definitio