PAIPAI

Tool Data

Stream typed progress and intermediate results from a tool.

Declare named channels with dataSchemas on a tool and write while it runs with ctx.data.write("progress", …). The tool card can render each update before the final output exists. Payloads are validated against the channel’s Zod schema and retained on that tool call.

server/tools/analyze.ts
import { defineTool } from "@pai/core";
import { z } from "zod";

export const analyze = defineTool({
  id: "analyze",
  description: "Analyze a document",
  inputSchema: z.object({ fileId: z.string() }),
  outputSchema: z.object({ summary: z.string() }),
  contextSchema: z.object({
    analyze: z.custom<(fileId: string) => Promise<string>>(),
  }),
  dataSchemas: {
    progress: z.object({ label: z.string(), percent: z.number() }),
  },
  execute: async (ctx) => {
    await ctx.data.write("progress", { label: "Reading", percent: 10 });
    const summary = await ctx.context.analyze(ctx.input.fileId);
    await ctx.data.write("progress", { label: "Done", percent: 100 });
    return { summary };
  },
});

The card reads channels from the tool part:

const progress = tool.part.data?.progress?.at(-1)?.value;

Keep payloads JSON. toModelOutput on a tool is a pure function of the durable result — it re-runs on replay, so it must not fetch or write:

defineTool({
  // ...
  toModelOutput: ({ output }) => ({
    type: "text",
    value: output.summary,
  }),
});

Next

On this page