PAIPAI

Tool State

Durable tool-part data, retries, and re-entry.

Tool state lives on the tool message part in the open tail assistant message.

Use tool data for durable progress, rich renderer state, and re-entry state. It is scoped to one tool call, typed by the tool's dataSchemas declarations, and visible to clients on the native tool part.

const analyzeFile = defineTool({
  id: "analyzeFile",
  description: "Analyze an uploaded file",
  inputSchema: z.object({
    fileId: z.string(),
  }),
  outputSchema: z.object({
    summary: z.string(),
  }),
  dataSchemas: {
    progress: z.object({
      label: z.string(),
      pct: z.number(),
    }),
  },

  execute: async (ctx) => {
    await ctx.data.write("progress", {
      label: "Reading file",
      pct: 0.2,
    });

    const result = await runExternalAnalysis({
      fileId: ctx.input.fileId,
      idempotencyKey: ctx.entry.type === "resume"
        ? ctx.entry.action.actionId
        : ctx.input.fileId,
    });

    await ctx.data.write("progress", {
      label: "Done",
      pct: 1,
    });

    return result;
  },
});

The same id updates the same data entry in place. Use stable ids for phases that can be retried or resumed:

await ctx.data.write("progress", { label: "Reading file", pct: 0.2 }, { id: "read" });
await ctx.data.write("progress", { label: "Reading file", pct: 0.6 }, { id: "read" });
await ctx.data.write("progress", { label: "Summarizing", pct: 0.8 }, { id: "summary" });

Clients receive grouped entries:

AssistantAI.useToolRenderer("analyzeFile", ({ data, state }) => {
  const progress = data.progress.records;

  return (
    <AnalysisPanel
      state={state}
      steps={progress.map((entry) => ({
        id: entry.id,
        ...entry.value,
      }))}
    />
  );
});

Durable tool data is included in snapshots and survives refresh. Transient tool data is live-only:

await ctx.data.write(
  "progress",
  { label: "Streaming bytes", pct: 0.45 },
  { id: "download", transient: true },
);

Use transient data only for cosmetic progress. Polling clients and refreshed pages may never see it.

PAI does not retry execute automatically. If code before a suspend point performs an external side effect, make that side effect idempotent in your own application service, using a stable key from the tool input or suspend action.

Tool data is not model-visible by default. Return values the model needs as tool output, expose values required for a wait point through suspend(), or append trusted messages from runtime/admin code.

Large durable resources should live in your application data model. Store stable ids or small render data on the tool part, not large bodies in the thread snapshot.

What Belongs In Tool Data

Good fits:

  • deployment phases and progress;
  • search result previews;
  • subagent activity summaries;
  • ids and labels for app-owned resources created by the tool;
  • resumable tool-local state needed after a suspend/resume cycle.

Poor fits:

  • file bodies, generated reports, or large JSON documents;
  • secret backend context;
  • data the model must rely on for correctness;
  • app resources that need their own permissions or lifecycle.

On this page