我们发现了什么
# PiDurableKit 面向 iPhone、iPad、Mac 和 Vision Pro 的持久化 AI 代理,由运行在 JavaScriptCore 中的 @earendil-works/pi-durable (https://www.npmjs.com/package/@earendil-works/pi-durable) 提供支持。 "") } - Swift 工具、系统提示段落、钩子与包装,均以普通闭包形式编写 - 子代理、带子任务的持久化任务以及原子事务 - 支
- 来源:GitHub(发现于 2026-10-09)
- 证据等级:D · 发现产品或需求信号,暂未获得可核验的商业证据。
- 商业模式:API / Usage-based
- 主题:独立产品
- 初筛评分:17.5/100 · 收录 1 次
证据,比故事更重要。
规则清洗与初筛,未经人工商业核验。原文语境、实际客户和付费情况仍需自行验证。
引用与数字披露
来源类型(原作者自述/第三方测算/媒体转引)需采集端标注,本版尚未落字段。
- 作者
- 未标注
- 抓取日期
- 来源类型
- 未标注
- 币种
- 未标注
- 口径
- 未标注
- 披露主体
- 未标注
- 披露日期
- 未标注
中文辅助译文(节选·触顶截断)
PiDurableKit
面向 iPhone、iPad、Mac 和 Vision Pro 的持久化 AI 智能体,基于在 JavaScriptCore 中运行的 @earendil-works/pi-durable。
对话、模型轮次、工具调用以及你自己的状态会在任何内容显示之前先提交到存储中。如果 iOS 在轮次中途杀死了你的应用,重新打开存储时会从中断的地方继续工作。
import PiDurableKit
let models = Models(credentials: .keychain)
try await models.setAPIKey(anthropicKey, for: .anthropic)
let harness = try await Harness.open(.sqlite(at: .documentsDirectory.appending(path: "agent.sqlite")), models: models)
let root = try await harness.root(agent: AgentChange(model: .anthropic("claude-sonnet-4-5")))
let submission = try await root.submit("What is the capital of France?")
let settled = try await submission.wait()
if case .done(_, let answer?) = settled.status {
let entry = try await root.commit { tx in try await tx.entry(answer) }
print(entry?.assistantMessage?.text ?? "")
}- 用纯闭包编写的 Swift 工具、系统提示段落、钩子(hook)与包装器(wrap)
- 子智能体、带有子任务的持久化任务,以及原子事务
- 所有 pi-ai 提供商(Anthropic、OpenAI、Google、OpenRouter、Groq、xAI、Mistral……)以及任何兼容 OpenAI 或 Anthropic 的服务器
- 支持崩溃恢复的 SQLite 或 JSONL 持久化,或内存存储
- 沙盒目录中 pi-durable 的
read/write/edit工具 - 流式输出:对话视图的
AsyncSequence(用于 SwiftUI)以及编码智能体风格的事件 - 引导(steering)、追问(follow-up)、中止、重置、压缩、分叉、类型化文档与条目、用量与成本
- 对 Claude Pro/Max、ChatGPT、GitHub Copilot 和 OpenRouter 进行 OAuth 登录,token 存放在 Keychain 中并自动刷新
- 用于测试和 SwiftUI 预览的脚本化虚拟提供商(faux provider)
需要 iOS 17、macOS 14、tvOS 17 或 visionOS 1。
安装
.package(url: "https://github.com/<you>/PiDurableKit.git", from: "1.0.0")并将 PiDurableKit 添加到你的 target 的依赖中。
概念
Swift API 与 pi-durable 的 API 一致,用 Swift 并发取代了 Chord 上下文(取消 Swift 任务即可取消等待;但取消等待不会取消工作)。
| 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(agent:), conversation.fork(at:) |
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 |
conversation.configure(change) | conversation.configure(AgentChange(…)) |
defineExtension, defineTool, section, hook | Extension, Tool, PromptSection, Hook |
wrapTool, wrapSection | Wrap.tool, Wrap.section |
| an extension written in JavaScript | Extension(name, javaScript: source) |
defineTask, TaskRuntime | TaskType, Phase, TaskRun |
harness.commit(), conversation.commit(), Tx | harness.commit { tx in … }, conversation.commit { tx in … }, Transaction |
ToolExecutionApi (commit, conversation, memo, …) | ToolCallContext |
conversation.watch() / viewState() | conversation.changes() / conversation.views(), conversation.view() |
watchEvents(harness, id) → snapshot, start(listener) | harness.watchEvents(id) → snapshot, for try await events in stream |
defineDoc, defineDocFamily + tx.doc() | Document(…, scope:, history:, keyed:, migrate:), update(_:_:), values(of:) |
defineEntry | EntryType, conversation.write(_:_:), entry.data(as:) |
taskGraph(), watchTaskGraph(), inspect(), getTask() | taskGraph(), taskGraphs(), inspect(), task(_:) |
HarnessOptions.env, CodingTools | ExecutionEnvironment.directory(_:) / .perConversation { target in … }, Extension.codingTools() |
harness.subscribeCommits() | harness.commits() |
SqliteStorage over a SqliteDatabase facade | Storage.sqlite(database:) with your SQLiteDatabase |
pi-ai models.completeSimple(), streamSimple() | models.complete(_:context:options:), models.stream(_:context:options:) |
ROOT_CONVERSATION_ID, ReadAfterWrite, StorageRejected | ConversationID.root, PiDurableError.readAfterWrite, .storageRejected |
conversationCreated, init, now, onReport | onConversationCreated:, initialize:, clock:, onReport: |
JsonlStorage, models.refresh() | Storage.jsonl(at:), models.refresh() |
工具
struct WeatherArguments: Decodable, Sendable { var city: String }
let weather = Tool(
"get_weather",
description: "Look up the current weather in a city",
parameters: .object(["city": .string("City name")])
) { (args: WeatherArguments, call) in
call.output("Looking up \(args.city)…\n") // streamed to the UI while the tool runs
return "Sunny and 22°C in \(args.city)" // a String, a ToolResult, or a JSONValue
}
let assistant = Extension("assistant") {
PromptSection("preamble", tag: false, text: "You are a concise assistant on an iPhone.")
PromptSection("today") { _ in Date.now.formatted(date: .complete, time: .omitted) }
weather
Hook.beforeTool { call, _ in
await askUserForApproval(call) ? .allow : .block("The user declined")
}
}
let harness = try await Harness.open(.sqlite(at: url), models: models, extensions: [assistant])每次调用都是其自身的持久化任务。抛出错误会向模型返回错误结果。被崩溃中断的调用只有在工具被声明为 replay: .safe 时才会在重启后重跑;否则模型会收到 interrupted 错误。返回 ToolResult.text(…).terminating() 以在不再发起模型请求的情况下结束本次运行。
通过 harness.install(_:) 和 harness.uninstall(_:) 在运行时安装、替换或移除扩展。通过 configure 按对话进行选择:
try await conversation.configure(AgentChange(
model: .openAI("gpt-4.1"),
thinkingLevel: .high,
extensions: .remove(assistant),
instructions: "Only answer in French."
))
try await conversation.configure(AgentChange(reset: [.extensions, .instructions]))JavaScript 扩展
用 JavaScript 编写的 pi-durable 扩展可以原样安装,就像 Node 应用安装自己的扩展一样:
let source = #"""
const { defineExtension, defineTool } = require("@earendil-works/pi-durable");
const { Type } = require("@earendil-works/pi-ai");
module.exports = defineExtension({
name: "dice",
tools: [defineTool({
name: "roll", description: "Roll a die", parameters: Type.Object({ sides: Type.Number() }),
execute: async (args) => ({ content: [{ type: "text", text: String(1 + Math.floor(Math.random() * args.sides)) }] }),
})],
});
"""#
try await harness.install(Extension("dice", javaScript: source))该模块是 CommonJS 格式,可以 require @earendil-works/pi-durable(以及 /tools、/env)、@earendil-works/pi-ai 和 @earendil-works/chord/context。与 pi-durable 一样,扩展代码没有沙箱保护,因此请只安装你信任的代码;它无法访问 PiDurableKit 自身的存储桥接。与任何扩展一样,在重启后恢复之前需要重新安装(Harness.open(…, resume: false),install,resume()),以便其待处理的工具调用能够继续。
PiDurable.documentation 是 pi-durable 自身的 README.md 和捆绑版本的 TypeScript 声明,因为其 npm 包就是附带它们的(捆绑包仅保留压缩后的代码)。就像 pi 让它的智能体指向 pi 的文档一样,把它交给负责编写扩展的智能体。
Examples/PiChat 同时使用这两者,让智能体将扩展写入其工作区并加载到正在运行的会话中:它将文档复制到 /docs/pi-durable,让一个提示段落指向它,并通过 load_extension 工具安装模块。该流程属于应用策略,不属于该包的一部分。
子智能体与事务
工具调用可以提交事务并驱动其他对话。这就是 pi-durable 的前台子智能体:子对话由该调用拥有,因此中止该调用会中止子任务,崩溃重跑时会找到同一个子任务。
let subagent = Tool("subagent", description: "Delegate a task", parameters: .object(["task": .string]), replay: .safe) {
(args: SubagentArguments, call) -> String in
let child = try await call.commit { tx in
if let existing = try await tx.conversations(ownedBy: call.taskId, limit: 1).items.first { return existing.id }
let created = try await tx.createConversation(ownedBy: call.taskId)
try await tx.configure(created.id, AgentChange(model: .anthropic("claude-haiku-4-5"), extensions: .remove(assistant)))
return created.id
}
let handle = try await call.conversation(child)!
let settled = try await handle.submit(args.task, requestId: "subagent:\(call.taskId)").wait()
guard case .done(_, let answer?) = settled.status else { return "\(settled.status)" }
return try await call.commit { tx in try await tx.entry(answer)?.assistantMessage?.text ?? "" }
}ToolCallContext 还提供 memo(在重跑中保留的值)、agent()、document(_:)、createTask、waitForTask,以及 details/diagnostic。结果可以 addingTools(…),工具可以修复参数(preparingArguments)或限制其输出(limitingOutput)。
harness.commit { tx in … } 和 conversation.commit { tx in … } 以原子方式执行任意一组读写操作:创建对话与分叉、追加条目、创建任务、配置智能体,以及读写文档。抛出错误会回滚所有内容。
持久化任务
struct Step: TaskCheckpoint { var phase: String } // a checkpoint names its next phase
struct Checkout: TaskCheckpoint { var phase = "pay"; var payments: [TaskID] = [] }
let payment = TaskType<String, Step, String>(
"app.payment", version: 1, initial: { _ in Step(phase: "charge") },
abort: { run in try await run.commit { _, _ in .aborted(nil) } }
) {
Phase("charge") { run in
let key = try await run.memo("idempotencyKey", default: UUID().uuidString) // stable across reruns
let receipt = try await charge(run.input, key: key)
try await run.commit { _, _ in .completed(receipt) }
}
}
let checkout = TaskType<[String], Checkout, [String]>(
"app.checkout", version: 1, initial: { _ in Checkout() },
abort: { run in try await run.commit { _, _ in .aborted(nil) } }
) {
Phase("pay") { run in
try await run.commit { tx, current in
var payments: [TaskID] = []
for card in run.input { payments.append(try await tx.createTask(payment, input: card, ownedBy: current.id)) }
return .waiting(Checkout(phase: "done", payments: payments), on: payments, policy: .failFast)
}
}
Phase("done") { run in
let receipts = try await run.outcomes(of: run.checkpoint.payments, as: String.self).compactMap(\.result)
try await run.commit { _, _ in .completed(receipts) }
}
}
let shop = Extension("shop") { payment; checkout }
let id = try await conversation.commit { tx in try await tx.createTask(checkout, input: ["visa", "amex"]) }
let outcome = try await harness.waitForTask(id, as: [String].self) // .completed, .failed, .aborted, …每次阶段切换都是一个检查点;崩溃后任务会从最后一个检查点继续。传入 version: 和 migrate: 以升级正在运行的任务,传入 abort: 以撤销任务的效果 wh
……(正文超出本站单页篇幅上限,此处截断;完整表述请见下方原文入口。)
译文由上游机器翻译生成,可能有误;判断请以英文原文为准。
英文原文(来源本站未改写)
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.
import PiDurableKit
let models = Models(credentials: .keychain)
try await models.setAPIKey(anthropicKey, for: .anthropic)
let harness = try await Harness.open(.sqlite(at: .documentsDirectory.appending(path: "agent.sqlite")), models: models)
let root = try await harness.root(agent: AgentChange(model: .anthropic("claude-sonnet-4-5")))
let submission = try await root.submit("What is the capital of France?")
let settled = try await submission.wait()
if case .done(_, let answer?) = settled.status {
let entry = try await root.commit { tx in try await tx.entry(answer) }
print(entry?.assistantMessage?.text ?? "")
}- 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/edittools in a sandboxed directory - Streaming:
AsyncSequences 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
.package(url: "https://github.com/<you>/PiDurableKit.git", from: "1.0.0")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(agent:), conversation.fork(at:) |
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 |
conversation.configure(change) | conversation.configure(AgentChange(…)) |
defineExtension, defineTool, section, hook | Extension, Tool, PromptSection, Hook |
wrapTool, wrapSection | Wrap.tool, Wrap.section |
| an extension written in JavaScript | Extension(name, javaScript: source) |
defineTask, TaskRuntime | TaskType, Phase, TaskRun |
harness.commit(), conversation.commit(), Tx | harness.commit { tx in … }, conversation.commit { tx in … }, Transaction |
ToolExecutionApi (commit, conversation, memo, …) | ToolCallContext |
conversation.watch() / viewState() | conversation.changes() / conversation.views(), conversation.view() |
watchEvents(harness, id) → snapshot, start(listener) | harness.watchEvents(id) → snapshot, for try await events in stream |
defineDoc, defineDocFamily + tx.doc() | Document(…, scope:, history:, keyed:, migrate:), update(_:_:), values(of:) |
defineEntry | EntryType, conversation.write(_:_:), entry.data(as:) |
taskGraph(), watchTaskGraph(), inspect(), getTask() | taskGraph(), taskGraphs(), inspect(), task(_:) |
HarnessOptions.env, CodingTools | ExecutionEnvironment.directory(_:) / .perConversation { target in … }, Extension.codingTools() |
harness.subscribeCommits() | harness.commits() |
SqliteStorage over a SqliteDatabase facade | Storage.sqlite(database:) with your SQLiteDatabase |
pi-ai models.completeSimple(), streamSimple() | models.complete(_:context:options:), models.stream(_:context:options:) |
ROOT_CONVERSATION_ID, ReadAfterWrite, StorageRejected | ConversationID.root, PiDurableError.readAfterWrite, .storageRejected |
conversationCreated, init, now, onReport | onConversationCreated:, initialize:, clock:, onReport: |
JsonlStorage, models.refresh() | Storage.jsonl(at:), models.refresh() |
Tools
struct WeatherArguments: Decodable, Sendable { var city: String }
let weather = Tool(
"get_weather",
description: "Look up the current weather in a city",
parameters: .object(["city": .string("City name")])
) { (args: WeatherArguments, call) in
call.output("Looking up \(args.city)…\n") // streamed to the UI while the tool runs
return "Sunny and 22°C in \(args.city)" // a String, a ToolResult, or a JSONValue
}
let assistant = Extension("assistant") {
PromptSection("preamble", tag: false, text: "You are a concise assistant on an iPhone.")
PromptSection("today") { _ in Date.now.formatted(date: .complete, time: .omitted) }
weather
Hook.beforeTool { call, _ in
await askUserForApproval(call) ? .allow : .block("The user declined")
}
}
let harness = try await Harness.open(.sqlite(at: url), models: models, extensions: [assistant])Each call is its own durable task. Throwing gives the model an error result. A call interrupted by a crash reruns after a restart only when the tool is declared replay: .safe; otherwise the model gets an interrupted error. Return ToolResult.text(…).terminating() to end the run without another model request.
Install, replace, or remove extensions at runtime with harness.install(_:) and harness.uninstall(_:). Choose per conversation with configure:
try await conversation.configure(AgentChange(
model: .openAI("gpt-4.1"),
thinkingLevel: .high,
extensions: .remove(assistant),
instructions: "Only answer in French."
))
try await conversation.configure(AgentChange(reset: [.extensions, .instructions]))JavaScript extensions
pi-durable extensions written in JavaScript install unchanged, the way a Node app installs its own:
let source = #"""
const { defineExtension, defineTool } = require("@earendil-works/pi-durable");
const { Type } = require("@earendil-works/pi-ai");
module.exports = defineExtension({
name: "dice",
tools: [defineTool({
name: "roll", description: "Roll a die", parameters: Type.Object({ sides: Type.Number() }),
execute: async (args) => ({ content: [{ type: "text", text: String(1 + Math.floor(Math.random() * args.sides)) }] }),
})],
});
"""#
try await harness.install(Extension("dice", javaScript: source))The module is CommonJS and can require @earendil-works/pi-durable (and /tools, /env), @earendil-works/pi-ai,
and @earendil-works/chord/context. As in pi-durable, extension code is not sandboxed, so install only code you trust;
it cannot reach PiDurableKit's own bridge to storage. Like any extension, install it again after a restart before
resuming (Harness.open(…, resume: false), install, resume()) so its pending tool calls continue.
PiDurable.documentation is pi-durable's own README.md and TypeScript declarations for the bundled version, as its
npm package ships them (the bundle keeps only minified code). Give it to an agent that writes extensions, the way pi
points its agent at pi's docs.
Examples/PiChat uses both to let the agent write extensions into its workspace and load them into the running
session: it copies the documentation to /docs/pi-durable, points a prompt section at it, and installs modules with a
load_extension tool. That flow is app policy, not part of the package.
Subagents and transactions
A tool call can commit transactions and drive other conversations. This is pi-durable's foreground subagent: the child conversation is owned by the call, so aborting the call aborts the child, and a crash-rerun finds the same child.
let subagent = Tool("subagent", description: "Delegate a task", parameters: .object(["task": .string]), replay: .safe) {
(args: SubagentArguments, call) -> String in
let child = try await call.commit { tx in
if let existing = try await tx.conversations(ownedBy: call.taskId, limit: 1).items.first { return existing.id }
let created = try await tx.createConversation(ownedBy: call.taskId)
try await tx.configure(created.id, AgentChange(model: .anthropic("claude-haiku-4-5"), extensions: .remove(assistant)))
return created.id
}
let handle = try await call.conversation(child)!
let settled = try await handle.submit(args.task, requestId: "subagent:\(call.taskId)").wait()
guard case .done(_, let answer?) = settled.status else { return "\(settled.status)" }
return try await call.commit { tx in try await tx.entry(answer)?.assistantMessage?.text ?? "" }
}ToolCallContext also offers memo (values that survive a rerun), agent(), document(_:), createTask,
waitForTask, and details/diagnostic. Results can addingTools(…), and tools can repair arguments
(preparingArguments) or bound their output (limitingOutput).
harness.commit { tx in … } and conversation.commit { tx in … } run any set of reads and writes atomically: create
conversations and forks, append entries, create tasks, configure agents, and read or write documents. Throwing rolls
everything back.
Durable tasks
struct Step: TaskCheckpoint { var phase: String } // a checkpoint names its next phase
struct Checkout: TaskCheckpoint { var phase = "pay"; var payments: [TaskID] = [] }
let payment = TaskType<String, Step, String>(
"app.payment", version: 1, initial: { _ in Step(phase: "charge") },
abort: { run in try await run.commit { _, _ in .aborted(nil) } }
) {
Phase("charge") { run in
let key = try await run.memo("idempotencyKey", default: UUID().uuidString) // stable across reruns
let receipt = try await charge(run.input, key: key)
try await run.commit { _, _ in .completed(receipt) }
}
}
let checkout = TaskType<[String], Checkout, [String]>(
"app.checkout", version: 1, initial: { _ in Checkout() },
abort: { run in try await run.commit { _, _ in .aborted(nil) } }
) {
Phase("pay") { run in
try await run.commit { tx, current in
var payments: [TaskID] = []
for card in run.input { payments.append(try await tx.createTask(payment, input: card, ownedBy: current.id)) }
return .waiting(Checkout(phase: "done", payments: payments), on: payments, policy: .failFast)
}
}
Phase("done") { run in
let receipts = try await run.outcomes(of: run.checkpoint.payments, as: String.self).compactMap(\.result)
try await run.commit { _, _ in .completed(receipts) }
}
}
let shop = Extension("shop") { payment; checkout }
let id = try await conversation.commit { tx in try await tx.createTask(checkout, input: ["visa", "amex"]) }
let outcome = try await harness.waitForTask(id, as: [String].self) // .completed, .failed, .aborted, …Every phase transition is a checkpoint; after a crash the task continues from the last one. Pass version: and
migrate: to upgrade live tasks, and abort: to undo a task's effects wh
... (truncated at the site's per-page length limit; see the source link below for the full text.)
这条还缺什么证据?
下面每条都由本条已有字段推出(等级、理由、商业模式、来源次数、是否演示), 本站不生成推测性结论;通用验证方法放在方法论页。
- 可核验的收入或付费证据查官网定价页与付费口径;第三方数据源(如 GetLatka)只作旁证,需标注来源与时点。
- 只有单一来源找一手站点或其他渠道是否重复出现同一产品;社区热帖数量不等于商业进展。
通用验证清单(谁有这个问题/谁愿意付费/一个人能交付哪一小步)见我们的筛选方法。