Pi Durable 推出跨環境 Agent 持久執行框架
Pi Durable
Pi Durable 讓 Agent 可跨環境長期執行,並支援多方協同調整,提升持久對話的可靠性。
Pi Durable 讓 Agent 可跨環境長期執行,並支援多方協同調整,提升持久對話的可靠性。
今天 Earendil 與 Pi 社群 釋出了 Pi 1.0。這體現了我們的信念:經過無數小時的硬化、維護與積極開發,Pi 現已成為可供構建的堅實基礎。Pi 仍在持續演進。與 Pi 1.0 一同,我們推出了一個實驗性新套件,名為 Pi Durable。Pi Durable 專為可長時間執行、耐用且可塑性代理而設計,能在任何地方執行。我們邀請你加入樂趣,協助我們打造最優秀的耐用工具。
為什麼 Pi Durable?
Pi 這個程式碼代理被設計為在您的(遠端)機器上、在終端機內執行,由一人驅動。如果程序死亡,您會檢視發生了什麼並告訴它繼續。這正是 Pi 1.0 專注並擅長的地方,且這不會改變。
在 Earendil,我們想將這項技術帶給每個人,無論以何種形式最符合他們的需求。為此,我們需要一個能在任何地方執行、可從不同介面訪問、支援無限長對話、能抵禦嚴重內部與外部失敗,並允許多位人類共同驅動同一代理的工具。
Pi Durable 就是那個工具。它並不取代 Pi 程式碼代理。它是一個用於構建任何代理式應用程式的框架,程式碼代理也包含在內。它不僅與 Pi 程式碼代理共享程式碼,例如 pi-ai,還共享其原則:最小化與可塑性。
它還讓我們能在此領域探索設計,而不會幹擾 Pi 程式碼代理。從在 Pi Durable 上構建代理式應用程式中學到的教訓,當證明有價值時,將迴流至 Pi 程式碼代理。
什麼是 harness?
每個人對於 harness 的定義各不相同。我們曾在之前 寫過此事,但讓我們重新介紹 Pi Durable 的 harness 概念。
一個 harness 是儲存與執行一個或多個大型語言模型對話所需機制的結合。它提供模型所呼叫的工具,以及工具執行的執行環境。
對話是您與代理之間的互動,並以文字記錄為轉錄。代理是大型語言模型及其設定,例如思考層級,以及它能呼叫的工具。
工具透過執行環境完成工作,執行環境可以是您的筆電、遠端虛擬機器,或是記憶體中的沙盒。代理所使用的工具與執行環境由每個對話決定。
harness 執行的所有工作,從呼叫模型到執行工具,都屬於一個任務。
與 Pi 的所有內容相似,Pi Durable 也是為了讓您的代理能夠理解而構建。整個原始碼(不含測試)約 15,000 行,使用 GPT 約 150,000 個 token,使用 Claude 約 250,000 個 token,這是最差情況。若要在 Pi Durable 上開發,您的代理很少需要全部程式碼;僅儲存後端就有 3,000 行,通常可以跳過。
現在讓我們帶您簡短參觀 Pi Durable,說明我們所構建的內容與原因。
長期執行於任何地方
我們希望代理能長時間執行,並能在任何地方執行,現在所謂任何地方指的是任何有 JavaScript 執行環境的地方。
在 Pi Durable 中,harness(測試框架)會在儲存後端上啟動。Pi Durable 附帶記憶體、SQLite 和 JSONL 儲存,以及符合性測試套件和基準測試,供您自訂後端使用。SQLite 與 JSONL 儲存程式碼不使用 Node API,因此只需一個小型介面卡即可在 Bun 或 Cloudflare Durable Object 內執行。儲存介面簡潔易實作,您可以在任何您擁有的系統上使用,例如 key-value 儲存或 Postgres。一次只能有一個程序擁有儲存,其他客戶端會連線到該程序。
在 SQLite 上,harness 只將工作集保留在記憶體中:即活躍的對話紀錄、正在執行的任務以及待提交的作業。其餘資料則儲存在磁碟上,直到需要時才載入。活躍對話紀錄自然受限於模型的上下文視窗,因為壓縮功能會在訊息溢位前先摘要舊訊息。因此,即使對話包含數萬條訊息,也能緊湊地放進記憶體。
需要檔案或 shell 的工具會從執行環境取得。Pi Durable 附帶 Node 執行環境,讓工具能存取您的本機檔案。與儲存相似,執行環境介面簡潔易實作,因此您也可以將遠端執行環境暴露給工具。這樣就能讓 harness 在一臺機器上執行,而工具則在另一臺機器上執行。您的 env 函式會為每一次工具呼叫建立環境,從對話的工作目錄開始,讓每個對話都能在不同的位置執行。
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { createModels } from "@earendil-works/pi-ai/models";
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
import { createRegistry, Harness } from "@earendil-works/pi-durable";
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
import {
openNodeSqliteStorage,
} from "@earendil-works/pi-durable/storage/sqlite/node";
import { CodingTools } from "@earendil-works/pi-durable/tools";
const context = BACKGROUND_CONTEXT; // every call takes a context for cancellation
const models = createModels();
models.setProvider(openaiProvider());
const registry = createRegistry();
registry.install(CodingTools); // read, write, edit, bash
const env = ({ cwd }: { cwd?: string }) =>
new NodeExecutionEnv({ cwd: cwd ?? process.cwd() });
const harness = await Harness.open(
await openNodeSqliteStorage("./agent.sqlite"),
{ models, registry, env },
context,
);
// The root conversation: created on first use, and the same one after every
// restart.
const root = await harness.root(context, {
agent: {
model: { provider: "openai", modelId: "gpt-6.1-sol" },
cwd: "/work/repo",
},
});
能夠在崩潰後存活
我們希望代理在程序終止時能夠存活,無論是筆記型電腦進入睡眠、容器被重新部署,或機器記憶體耗盡,都能從中斷處繼續執行。
在 Pi Durable 中,執行的每一步都是一個任務,並在繼續前儲存一個檢查點。若程序死亡,新的程序會開啟同一個儲存,尋找未完成的任務,並從最後的檢查點繼續。被截斷的模型請求會重新送出;部分答案會保留在對話紀錄中,並標記為中斷。若工具呼叫被截斷,且安全可重試,則會重新執行;否則模型會被告知已被中斷。Pi Durable 沒有內建子代理,但只需幾行程式碼即可構建,以下的 triage 工具示範。子代理在自己的對話中執行,亦能從中斷處繼續;且安全可重試的子代理工具會再次找到其子代理並等待其答案。排隊訊息仍保持排隊。requestId 以一次性提交方式執行,因此在崩潰後重試的客戶端會收到原始提交,而非重複請求。
const job = {
type: "input",
content: "Fix the flaky login test",
requestId: "job-42",
} as const;
await root.submit(job, context);
// The process dies here, in the middle of a tool call.
// A new process opens the same storage.
const harness = await Harness.open(
await openNodeSqliteStorage("./agent.sqlite"),
{ models, registry, env },
context,
);
harness.resume(); // continue the interrupted run
const root = await harness.root(context);
// the same submission, answered
const settled = await (await root.submit(job, context)).wait(context);
同時處理多個對話
我們希望一個 harness 能同時執行多個對話,互不阻塞。
在 Pi Durable 中,一個 harness 可以同時執行任意數量的對話,並行執行,且享有相同的保證。對話可以從新開始,也可以在任意位置從另一個對話分支,並在不複製的情況下看到父對話到該點的歷史。
想像一個 Slack 頻道,您的代理會回覆任何人的提及。接著有人開啟一條子討論串。頻道可視為一個對話,而子討論串則是以該訊息為分支的子對話。兩者同時執行,互不阻塞。
const channel = await harness.root(context);
const question = await channel.submit(
{ type: "input", content: "@agent why did the deploy fail?" },
context,
);
const answered = await question.wait(context);
// Someone replies to the agent's answer in a thread. Every conversation names
// its owner, which decides what an abort reaches (more on that under Tasks).
// The thread has none.
const thread = await channel.fork(
answered.answer!,
{ ownership: { kind: "ownerless" } },
context,
);
// Both conversations work at the same time.
const inThread = await thread.submit(
{ type: "input", content: "@agent can we roll it back?" },
context,
);
const inChannel = await channel.submit(
{ type: "input", content: "@agent who is on call today?" },
context,
);
await Promise.all([inThread.wait(context), inChannel.wait(context)]);
每個對話還會儲存自己的代理設定:模型、思考層級、已選擇的擴充功能以及其工具的啟用狀態、額外指示,以及執行環境中的工作目錄。與主代理並列的審核者可使用較便宜的模型、唯讀工具,並擁有自己的檢出。
擴充功能
我們希望代理能做的所有事情都能被插拔化,並且每個已插入的元件都能參與耐久性。
在 Pi Durable 中,擴充功能是一組命名的系統提示段落、工具、掛鉤和任務。應用程式將擴充功能安裝到登錄檔中。每段對話會選擇使用哪些擴充功能和工具,並僅儲存它們的名稱。
系統提示段落
系統提示會在每次請求前從對話的擴充功能段落重新構建,這樣改動的段落會在下一個請求中被採用。Pi Durable 會在文字記錄中記錄變更的位置,確保重新啟動或分叉時能看到模型所見的精確內容。對於支援在對話中間變更系統提示和工具的模型,僅傳送變更,保持提示快取有效。
import { defineExtension, section } from "@earendil-works/pi-durable";
const ProjectContext = defineExtension({
name: "project-context",
sections: [
// Read from the conversation's execution environment. The files can be
// loaded and watched in the background; every request renders the
// latest state.
section("agents_md", (input) => agentsMd.latest(input.env)),
section("skills", (input) => skills.latest(input.env)),
],
});
工具
每一次工具呼叫都作為獨立的耐久任務執行,其意圖會在執行前儲存。若發生崩潰,工具僅在表示安全時才會重新執行。否則模型會被告知該呼叫已被中斷,並附上目前已儲存的輸出,然後決定接下來的行動。每段對話也可擁有自己的工具集合,例如前述的 Slack 佈線,該工具可搜尋但不執行部署。
import { Type } from "@earendil-works/pi-ai";
import { defineTool } from "@earendil-works/pi-durable";
const searchIssues = defineTool({
name: "search_issues",
description: "Search the issue tracker",
parameters: Type.Object({ query: Type.String() }),
replay: "safe", // only reads, so a rerun after a crash is fine
execute: async (args, api) => {
// streamed to every client watching
api.output(`searching for ${args.query}\n`);
return {
content: [{ type: "text", text: await tracker.search(args.query) }],
};
},
});
const deploy = defineTool({
name: "deploy",
description: "Deploy a version to production",
parameters: Type.Object({ version: Type.String() }),
// No replay: a deploy interrupted by a crash is reported to the model,
// never repeated.
execute: async (args) => ({
content: [{ type: "text", text: await ci.deploy(args.version) }],
}),
});
registry.install(defineExtension({ name: "ops", tools: [searchIssues, deploy] }));
// The thread may search, but not deploy.
await thread.configure({ tools: { remove: [deploy] } }, context);
工具會取得其呼叫所需的 harness API:可提交條目與檔案、啟動任務與對話,並與其他對話溝通。這使得子代理只需幾行程式碼即可實作。工具會建立它所擁有的對話,為其分配較小的模型與自身指令,並等待其回覆。子代理本質上是一段對話,因而能在崩潰後存活、計算自身成本,且使用者介面可在呼叫下顯示它。
import type { AssistantMessage } from "@earendil-works/pi-ai";
import { AssistantEntry, configure } from "@earendil-works/pi-durable";
const triage = defineTool({
name: "triage",
description: "Label an incoming issue as bug, feature, or question",
parameters: Type.Object({ issue: Type.String() }),
// a rerun after a crash finds the same subagent and the same submission
replay: "safe",
execute: async (args, api, context) => {
const child = await api.commit(async (tx) => {
const existing = (
await tx.scanConversations({ ownerTaskId: api.taskId }, 1)
).items[0];
if (existing !== undefined) return existing.id;
// Owned by this call, so aborting the call aborts the subagent.
const created = await tx.createConversation({
ownership: { kind: "task", taskId: api.taskId },
});
// It starts as a copy of this conversation's agent. Make it a small
// model without tools.
await configure(tx, created.id, {
model: { provider: "openai", modelId: "gpt-6-luna" },
tools: [],
instructions: "Answer with one word: bug, feature, or question.",
});
return created.id;
}, context);
// lets a UI show the subagent under the call
await api.details({ conversationId: child }, context);
const subagent = await api.conversation(child, context);
const request = {
type: "input",
content: args.issue,
requestId: `triage:${api.taskId}`,
} as const;
const settled = await (
await subagent!.submit(request, context)
).wait(context);
// The answer is an entry in the subagent's transcript. Read it and take
// its text.
const entry = await api.commit(
(tx) => tx.entry(AssistantEntry, settled.answer!),
context,
);
const message = entry?.model?.[0] as AssistantMessage;
const text = message.content
.flatMap((content) => (content.type === "text" ? [content.text] : []))
.join("");
return { content: [{ type: "text", text }] };
},
});
擴充功能也能改變其他擴充功能的工具。若後續擴充功能中有相同名稱的工具,將取代先前的,例如在 Python 虛擬環境中執行的 bash。包裝功能會裝飾獲勝的工具,無論包裝擴充功能被選取於何處。
import { wrapTool } from "@earendil-works/pi-durable";
import { createBashTool } from "@earendil-works/pi-durable/tools";
// Times every bash call, whichever bash the conversation ends up with.
const Timing = defineExtension({
name: "timing",
wraps: [
wrapTool(createBashTool(), (bash) => ({
...bash,
execute: async (args, api, context) => {
const start = Date.now();
try {
return await bash.execute(args, api, context);
} finally {
metrics.record("bash", Date.now() - start);
}
},
})),
],
});
掛鉤
掛鉤允許擴充功能介入任務,包括生成模型回覆、呼叫工具或執行壓縮的內建任務。它們能在請求送往模型前重寫請求、阻止或重寫工具呼叫、取代結果、保持執行繼續,或自行撰寫摘要。掛鉤在崩潰後可再次執行,故做出決策的掛鉤會將其存於備忘錄:一個與任務一起儲存的小值,首次寫入勝出。
import { hook, ToolTask } from "@earendil-works/pi-durable";
const Approval = defineExtension({
name: "approval",
hooks: [
hook(ToolTask, {
beforeTool: async (call, api, context) => {
if (call.name !== "deploy") return undefined;
// After a restart, the hook finds the stored answer instead of
// asking again.
let approved = await api.memo<boolean>(
"approval:deploy",
context,
);
approved ??= await api.memo(
"approval:deploy",
await askInSlack(call),
context,
);
return approved
? undefined
: { block: "Nobody approved the deploy." };
},
}),
],
});
多個擴充功能可掛鉤相同專案。它們的掛鉤以鏈結方式執行,順序為對話所選擇的擴充功能順序,每個掛鉤定義其鏈的運作方式。beforeTool 將重寫後的引數傳遞至鏈下,第一個阻塞即可停止。afterTool 將結果傳遞至鏈下。onYield 在第一個保持執行的掛鉤停止。觀察者如afterResponse 始終執行全部。丟擲例外的掛鉤會被報告,鏈仍繼續,除非beforeTool,此時丟擲會阻止呼叫。
任務
harness 以內建任務執行對話:每次模型請求一個、每次工具呼叫一個、以及一次壓縮任務。擴充功能帶來自己的任務,並使用相同機制:每一步後的檢查點、能在重啟後持續的計時器,以及等待其他任務。
將帳單拆分至多張卡片的結帳一次性扣款所有卡。若其中一張卡被拒,其他付款將被中止並自動退款。
import { defineTask, type TaskId } from "@earendil-works/pi-durable";
const Payment = defineTask<{ card: string }, { phase: "charge" }, string>({
name: "shop.payment",
version: 1,
initial: () => ({ phase: "charge" }),
phases: {
charge: async (task, runtime, context) => {
// The key makes the charge idempotent: if a crash reruns this
// phase, the card is only charged once.
const charge = await bank.charge(
task.input.card,
`payment-${task.id}`,
);
await runtime.commit(
() => ({
status: "terminal",
outcome: charge.ok
? { status: "completed", result: charge.receipt }
: { status: "failed", error: { message: charge.error } },
}),
context,
);
},
},
// Another payment failed, or the checkout was cancelled: undo this one.
abort: async (task, runtime, context) => {
await bank.refund(`payment-${task.id}`);
await runtime.commit(
() => ({ status: "terminal", outcome: { status: "aborted" } }),
context,
);
},
});
type CheckoutState =
| { phase: "pay" }
| { phase: "decide"; payments: TaskId<string>[] };
const Checkout = defineTask<{ cards: string[] }, CheckoutState, string>({
name: "shop.checkout",
version: 1,
initial: () => ({ phase: "pay" }),
phases: {
pay: async (task, runtime, context) => {
await runtime.commit(async (tx) => {
const payments: TaskId<string>[] = [];
for (const card of task.input.cards) {
payments.push(
await tx.createTask(Payment, { card }, {
ownership: { kind: "task", taskId: task.id },
}),
);
}
// Run no code until every payment is done. The first failed
// payment aborts the others.
return {
status: "waiting",
checkpoint: { phase: "decide", payments },
on: payments,
policy: "failFast",
};
}, context);
},
decide: async (task, runtime, context) => {
const outcomes = await runtime.outcomes(
task.state.checkpoint.payments,
context,
);
const paid = outcomes.every(
(outcome) => outcome.status === "completed",
);
await runtime.commit(
() => ({
status: "terminal",
outcome: paid
? { status: "completed", result: "Order placed." }
: {
status: "failed",
error: { message: "A payment failed." },
},
}),
context,
);
},
},
abort: (_task, runtime, context) =>
runtime.commit(
() => ({ status: "terminal", outcome: { status: "aborted" } }),
context,
),
});
// The agent starts a checkout with a tool.
const checkout = defineTool({
name: "checkout",
description: "Pay for the cart, split across several cards",
parameters: Type.Object({ cards: Type.Array(Type.String()) }),
execute: async (args, api, context) => {
// Owned by this call: aborting the call aborts the checkout and refunds
// its payments.
const owner = {
ownership: { kind: "task", taskId: api.taskId },
} as const;
const id = await api.createTask(
Checkout,
{ cards: args.cards },
owner,
context,
);
const { outcome } = (await api.waitForTask(id, context)).state;
const text =
outcome.status === "completed" ? outcome.result : outcome.status;
return { content: [{ type: "text", text }] };
},
});
registry.install(defineExtension({
name: "shop",
tools: [checkout],
tasks: [Payment, Checkout],
}));
任務與對話構成一棵所有權樹。中斷任務會中斷其所擁有的內容,從下往上,因而每個任務會先清理自己的影響,並且只有當其所擁有的工作完成後才算結束。子代理遵循相同模式:由啟動它的工具呼叫所擁有的對話。
任務預設為前景:它們屬於對話的當前工作。對話只有在它們完成後才會變為閒置,並且中斷對話(例如使用者按下 Esc)會中斷它們及其所擁有的一切。背景任務屬於對話,但不屬於其當前工作。對話在背景任務執行時會保持閒置,而普通中斷則不會影響它及其所擁有的一切。這適用於應該存活於啟動它的回合之後的子代理,或是明日觸發的提醒。中斷任務本身,或使用 { background: true } 中斷對話,仍會停止它。
// Part of the current work: Esc aborts it, and the conversation waits for it.
await api.createTask(
Checkout,
input,
{ ownership: { kind: "task", taskId: api.taskId } },
context,
);
// Side work: the conversation goes idle while it runs, and Esc leaves it alone.
await api.createTask(
Reminder,
input,
{ ownership: { kind: "conversation" }, background: true },
context,
);
壓縮
我們希望長對話能持續進行,而不必讓代理停下來做摘要。
在 Pi Durable 中,壓縮是一個與其他任務相同的任務,並且在對話持續進行時執行。當上下文接近模型限制時,背景壓縮會摘要較舊的訊息,並將摘要置於下一個回合邊界。對話僅在下一個請求無法容納時才等待摘要。若供應商仍以「過長」拒絕請求,系統會進行壓縮並重試一次。你亦可隨時手動壓縮,並提供自己的指示。舊訊息始終保留在儲存中。
const harness = await Harness.open(storage, {
models,
registry,
settings: {
compaction: {
// past contextWindow - reserveTokens, the next request waits for a
// summary
reserveTokens: 16384,
// this far before that, a summary starts in the background
backgroundTokens: 32768,
},
},
}, context);
// Manual, also while the agent is working.
await root.compact("Keep the names of the failing tests", context);
reset() 更進一步:它會啟動一個新的上下文,可選擇從交接筆記開始,工具亦可透過回傳 control: { handoff } 來要求相同。因為沒有任何資料被刪除,第二個工具仍能搜尋交接前的所有內容。這就是建立能自我交接並稍後查詢的代理所需的全部。
const handoff = defineTool({
name: "handoff",
description:
"Start over from a handoff note. " +
"Older messages stay searchable with search_history.",
parameters: Type.Object({ note: Type.String() }),
execute: async (args, api, context) => {
// Queued behind the handoff, so it starts the next run in the new
// context.
const self = await api.conversation(api.conversationId, context);
await self!.submit(
{
type: "input",
content: "Continue.",
requestId: `handoff:${api.taskId}`,
},
context,
);
// Ends this run and starts a new context from the note, like
// reset(note).
return {
content: [{ type: "text", text: "Handing off." }],
control: { handoff: args.note },
};
},
});
const searchHistory = defineTool({
name: "search_history",
description: "Search older messages, including those before a handoff",
parameters: Type.Object({ text: Type.String() }),
replay: "safe",
execute: async (args, api, context) => {
// Tools read records through a transaction too. One that writes
// nothing stores nothing.
const page = await api.commit(
(tx) => tx.scanEntries({ conversationId: api.conversationId }, 200),
context,
);
const hits = page.items.filter((entry) =>
JSON.stringify(entry.model ?? []).includes(args.text),
);
const text = hits.map((entry) => JSON.stringify(entry.model)).join("\n");
return { content: [{ type: "text", text }] };
},
});
耐久應用程式狀態
我們希望建立在代理上的應用程式狀態與對話本身同樣耐久。
在 Pi Durable 中,應用程式狀態(例如待辦清單、計畫、工單,或對話所執行的沙盒)儲存在檔案中。檔案是帶型別的 JSON,與對話紀錄一起儲存,並在同一個原子提交中變更,確保狀態永遠與產生它的對話紀錄保持一致。每個檔案都說明分支起始時的內容:父節點在分支點的值、其目前值或一個全新的值。
import { defineDoc } from "@earendil-works/pi-durable";
const Todos = defineDoc<{ items: string[] }>({
kind: "app.todos",
version: 1,
scope: "conversation",
history: "rewindable",
fork: "asOf", // a fork starts with the todos its parent had at the fork entry
initial: () => ({ items: [] }),
});
const Todo = defineExtension({
name: "todo",
tools: [
defineTool({
name: "todo",
description: "Add an item to your todo list",
parameters: Type.Object({ item: Type.String() }),
execute: async (args, api, context) => {
await api.commit(async (tx) => {
const todos = await tx.doc(Todos, api.conversationId);
todos.items.push(args.item);
}, context);
const text = `Added ${args.item}`;
return { content: [{ type: "text", text }] };
},
}),
],
// The model sees the list before every request.
sections: [
section("todos", async (input, context) => {
const todos = await input.read.snapshot(
Todos,
input.conversationId,
context,
);
return todos?.items.join("\n") || undefined;
}),
],
});
// A UI subscribes to the committed value.
const todos = await harness.documentState(Todos, channel.id, context);
todos?.subscribe((value) => renderTodos(value?.items ?? []));
可塑性
我們希望在不停止代理的情況下改變其程式碼。
在 Pi Durable 中,登錄檔可在對話執行時變更。以已安裝的名稱安裝擴充功能會一步驟替換它。已在執行中的工具呼叫會以其啟動時的程式碼完成;下一個呼叫則使用新程式碼。對話只儲存擴充功能和工具名稱,從不儲存程式碼,因此重新啟動後會接續使用新程序安裝的內容。
// The extension's file changed on disk.
// same name "ops": replaces the installed one
registry.install(await loadExtension("./ops.ts"));
多人模式
我們希望多位使用者和客戶端能同時使用同一對話:觀察、延遲加入並引導它們。
在 Pi Durable 中,UI 所需的一切皆為已提交的狀態,因此任意數量的客戶端都能連線到 harness 中的任何對話。客戶端首先取得目前的畫面:對話紀錄、正在串流的答案、正在執行的工具及其輸出、佇列訊息、代理與使用量。此後它只會接收變更。延遲加入或重新連線的客戶端會從目前畫面開始。任何客戶端都能引導正在執行的對話或排隊後續。
// A second client joins the thread while the agent is working.
const view = await thread.viewState(context);
render(view.value);
view.subscribe((value) => render(value));
// And steers it. The message joins the running work after the current tool
// calls.
await thread.submit(
{ type: "input", content: "Check the staging logs first", whenBusy: "steer" },
context,
);
對於遠端客戶端,thread.watch() 交付每一次提交的精確操作,足夠小以便透過 socket 傳送。如果你偏好程式碼代理的熟悉事件,watchEvents() 將提交轉換為那些事件,會增加網路傳輸的位元數。
試試看
你可以今天就試用 Pi Durable。Pi Durable 仍為實驗性,API 仍可能變更。將你的代理指向 Pi checkout 中的 packages/durable,讓它閱讀
README、超過三十個
範例、Pi Durable 上的小型程式碼代理,或這個美麗的 度假規劃代理,並開始構建。
度假規劃器大約 1,300 行 TypeScript,主要是 TUI。 若看起來像程式碼代理,只是因為它借用了 Pi 程式碼代理的 TUI 元件。
- 一個帶有 TUI 的度假規劃器,建立於 Pi Durable 上。
- 一個子代理同時執行三個搜尋,每個都是耐久任務。
- 同時,主代理可以自由聊天。
- 該流程已結束。天氣與博物館已完成,列車尚未完成。
- 重新啟動。
search可以安全重跑:只有列車再次執行。 - 切換到子代理並指導它。
- 回到主代理:詢問、簡化並指導,同時它仍在運作。
- 報告以訊息形式到達;主代理將其轉為計畫。
要從 Pi checkout 執行兩個示範:
npm install && npm run build
node packages/coding-agent/src/experimental/durable/main.ts
node packages/coding-agent/src/experimental/vacation/main.ts
在自己的專案中使用 Pi Durable 建置:
npm install @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
在接下來的幾週,我們將更深入討論 Pi Durable,並展示我們用它構建的小型代理工具,協助我們工作,例如 Slack 機器人或 GitHub triage 機器人。我們暫時不想透露太多。隨著我們像使用 Pi 一樣使用 Pi Durable,還會有更多內容推出。
常見問題
為什麼再次使用 TypeScript?
因為它是啟動此專案最簡單的方法。但正如大家現在已知的,將一切移植到 Rust 或組譯語言都非常容易。我們並未排除未來的可能,但目前我們專注於 TypeScript。
來源:hackernews100 · earendil.com