Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/docs/pi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,6 @@ The `pi()` function accepts every [`actor()`](/actors/docs/actor-configuration)
| `settings` | Pi Durable settings, such as `retry` and `compaction`. |
| `onAnswer` | Runs for every final answer. See [Deliver answers](#deliver-answers). |

Other options, such as `thinkingLevel` and `env`, pass through to Pi Durable. By default, `actionTimeout` is ten minutes and `sleepGracePeriod` is 15 minutes. A `ConversationBusy` error reaches clients as a `UserError`, and other Pi Durable errors as internal errors.
Other options, such as `thinkingLevel` and `env`, pass through to Pi Durable. By default, `actionTimeout` is ten minutes and `sleepGracePeriod` is 15 minutes. A `ConversationBusy` error reaches clients as a `UserError`, and so does `pi_schema_too_new` when the actor's data has a newer Pi Durable schema than the code. Other Pi Durable errors reach clients as internal errors.

**Next:** [Design Patterns](/agents/docs/design-patterns), how to structure an app with many agents.
21 changes: 18 additions & 3 deletions packages/pi/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { ConversationBusy } from "@earendil-works/pi-durable";
import { UserError } from "rivetkit";

/** Pi Durable 1.4 has no typed error for this, so its message is the only signal. */
const NEWER_SCHEMA =
/SQLite schema version (\d+) is newer than supported version (\d+)/;

/**
* The error a client receives for a failed Pi Durable call. `ConversationBusy`
* is the one Pi error a caller is meant to handle, so it becomes a
* `UserError` with Pi's error name as the code. Every other error is returned
* as is, and RivetKit reports it to clients as an internal error.
* becomes a `UserError` with Pi's error name as the code, because a caller can
* handle it. Storage from a newer Pi Durable schema, such as after a rollback,
* becomes a `UserError` with the code `pi_schema_too_new`, because no retry
* can fix it. Every other error is returned as is, and RivetKit reports it to
* clients as an internal error.
*/
export function toClientError(error: unknown): unknown {
if (error instanceof ConversationBusy) {
Expand All @@ -15,5 +21,14 @@ export function toClientError(error: unknown): unknown {
cause: error,
});
}
const newerSchema =
error instanceof Error ? NEWER_SCHEMA.exec(error.message) : null;
if (newerSchema) {
const [, stored, supported] = newerSchema;
return new UserError(
`This actor's Pi Durable data has schema version ${stored}, and the deployed code supports up to version ${supported}. Deploy a @rivet-dev/pi version that supports it. Pi Durable data can't roll back.`,
{ code: "pi_schema_too_new", cause: error },
);
}
return error;
}
14 changes: 10 additions & 4 deletions packages/pi/tests/upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ describe("pi drain and upgrades", () => {
});
});

test("storage written by a newer Pi Durable schema makes Pi's actions fail with an internal error, while the app's own actions and hooks still run", async (c) => {
test("storage written by a newer Pi Durable schema makes Pi's actions fail with pi_schema_too_new, while the app's own actions and hooks still run", async (c) => {
const { client } = await setupTest(c, registry);
const key = ["rollback", randomUUID()];
const handle = client.migrating.getOrCreate(key);
Expand All @@ -483,10 +483,16 @@ describe("pi drain and upgrades", () => {
await handle.nap();
await sleeps.waitFor(key, 1);

// The caller cannot fix a rollback, so RivetKit hides Pi's message; the actor logs it.
await expect(handle.harness.inspect()).rejects.toMatchObject({
group: "rivetkit",
code: "internal_error",
group: "user",
code: "pi_schema_too_new",
message: expect.stringMatching(
/schema version 3.*supports up to version 2/,
),
});
await expect(handle.startJob()).rejects.toMatchObject({
group: "user",
code: "pi_schema_too_new",
});
await handle.nap();
await sleeps.waitFor(key, 2);
Expand Down
Loading