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.
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,
}),
});