PAIPAI

Agent Definitions

The @pai/core agent definition API.

Package: @pai/core

defineAgent

function defineAgent<TConfig extends AgentConfig>(
  config: TConfig,
): AgentDefinition<InferRegisteredAgentContract<TConfig>>;

defineAgent() creates the server-side definition for an agent. It does not create storage, HTTP routes, workers, or realtime subscriptions.

When tools or commands declare capabilities, the returned definition also carries their inferred id-keyed requirement map in its third type parameter — see AgentDefinition.

The model field accepts the AI SDK LanguageModel surface directly, a ModelSelection, or a resolver that returns either form. PAI owns the runtime translation between persisted thread messages and AI SDK model messages, but it does not ask consumers to implement a PAI-specific model adapter.

const assistantAgent = defineAgent({
  name: "report",
  version: "1.0.0",
  identity: workspaceScopeSchema,
  clientData: clientDataSchema,
  runMetadata: {
    private: z.object({ evaluationJobId: z.string().optional() }),
    public: z.object({ rating: z.enum(["up", "down"]).optional() }),
  },
  threadMetadata: {
    private: z.object({ contextCursor: z.string().optional() }),
    public: z.object({ category: z.string().optional() }),
  },
  messageMetadata: {
    private: z.object({ reviewedBy: z.string().optional() }),
    public: z.object({ citations: z.array(z.string()).optional() }),
  },
  uiDataSchemas: {
    "report-progress": z.object({
      label: z.string(),
      percent: z.number().min(0).max(100),
    }),
  },
  model: ({ runtimeContext }) => runtimeContext.models.primary,
  instructions: ({ runtimeContext }) =>
    `Draft reports for ${runtimeContext.workspace.name}.`,
  runtimeContext: async ({ identity, clientData, signal }) => ({
    workspace: await loadWorkspace(identity.workspaceId, { signal }),
    clientData,
    models,
  }),
  tools: {
    requestApproval,
    getSelectedText,
  },
});

AgentConfig

import type { Instructions, LanguageModel, ToolSet } from "ai";
import type { JsonValue } from "@pai/protocol";
import type { z } from "zod";

type AgentConfig = {
  /** Stable agent identifier used in manifests, diagnostics, and contract checks. */
  name: string;

  /** Optional application-controlled version for deploy and compatibility checks. */
  version?: string;

  /**
   * Trusted server-resolved caller for a request. The partition key and the
   * actor key are both derived from this in `createPai`.
   * This is validated with Zod after `resolveIdentity()` computes it.
   */
  identity?: z.ZodType;

  /**
   * Serializable request metadata supplied by the client.
   * This is untrusted and must not be used for access decisions.
   */
  clientData?: z.ZodType;

  /** Typed server-only and client-readable metadata carried by every run. */
  runMetadata?: RunMetadataSchemas;

  /** Typed server-only and client-readable metadata carried by the thread. */
  threadMetadata?: ThreadMetadataSchemas;

  /**
   * Typed server-only and client-readable metadata carried by each message.
   *
   * The public bag is projected onto every client message shape, typed as
   * `MessageMetadataOf<TContract>`. The private bag appears on no client shape
   * at all. Trusted lifecycle reads expose both on `LifecycleMessage`, while
   * ordinary `PaiMessage` values include only the public fields plus the
   * reserved `metadata.pai` namespace.
   */
  messageMetadata?: MessageMetadataSchemas;

  /**
   * Closed schemas for application-authored native AI SDK `data-*` parts.
   * Each parsed payload must be strict JSON and stable when parsed again.
   */
  uiDataSchemas?: Record<string, z.ZodType<JsonValue>>;

  /** AI SDK model, configured selection, or context-aware selector. */
  model:
    | LanguageModel
    | ModelSelection
    | ((
        input: AgentBehaviorInput,
      ) =>
        | LanguageModel
        | ModelSelection
        | Promise<LanguageModel | ModelSelection>);

  /** AI SDK instructions or a builder evaluated for each model step. */
  instructions:
    | Instructions
    | ((input: AgentBehaviorInput) => Instructions | Promise<Instructions>);

  /** Builds runtime-only agent state from identity and client data. */
  runtimeContext?: AgentRuntimeContextBuilder;

  /**
   * Static tools the agent may make available to the model.
   * Tools without `execute` can be fulfilled by clients or manual submitters.
   */
  tools?: Record<string, ToolDefinition | AgentToolConfig>;

  /**
   * Static AI SDK provider-defined tools. Their schemas remain the authority
   * for retained native messages and are included in the inferred contract.
   */
  providerTools?: ToolSet;

  /** Advanced model/tool lifecycle hooks and stream transforms. */
  lifecycle?: AgentLifecycle;

  /** Maximum model-loop steps per episode. Defaults to 10; `prepareRun` may override it. */
  maxSteps?: number;

  /** Opt-in persisted thread title generation after completed runs. */
  threadTitles?: false | true | ThreadTitleConfig;

  /** Opt-in short labels for reasoning parts after each reasoning part closes. */
  reasoningSummary?: false | true | ReasoningSummaryConfig;
};

uiDataSchemas is the closed registry for application-authored native data parts. A key such as report-progress produces a typed data-report-progress part. PAI validates each payload through its registered schema, requires the parsed value to be strict JSON and stable when parsed again, and exposes the inferred payload under AgentContract["uiData"].

Telemetry is runtime infrastructure rather than agent behavior. Configure PAI-owned orchestration telemetry on createPai or createAgentRuntime, and configure AI SDK model-call telemetry independently of the agent definition.

Instructions is the AI SDK's exact instruction value: a string, one system message, or an ordered array of system messages. Structured messages may carry message-local providerOptions, such as a provider cache hint. Provider support for multiple system messages and specific options varies.

The root instruction builder is evaluated while assembling each primary model-step request unless prepareRun supplies an episode-level replacement. Lifecycle patches replace the complete instruction value; PAI does not merge instruction arrays.

A text-bearing prepareRun response also resolves the root instructions when transformStepStream is configured, because that hook receives a synthetic model-step request. No model provider call is made for that response.

Thread-title instruction builders run for each scheduled default title call. Reasoning-summary instruction builders run for each closed reasoning part sent to the default summarizer. A custom generator bypasses the corresponding model and instruction fields.

Message-local provider options stay attached to their instruction message. If a lifecycle patch changes provider families, replace instructions in the same patch when the current structured value contains provider-specific options.

Run Metadata

Pass a raw pair of Zod schemas when an agent owns typed data at run scope:

import { z } from "zod";

type RunMetadataSchema = z.ZodType<Record<string, unknown>>;

type RunMetadataSchemas<
  TPrivate extends RunMetadataSchema,
  TPublic extends RunMetadataSchema,
> = Readonly<{
  private: TPrivate;
  public: TPublic;
}>;
import { defineAgent } from "@pai/core";
import { z } from "zod";

export const reportRunMetadata = {
  private: z.object({
    evaluationJobId: z.string().optional(),
  }),
  public: z.object({
    rating: z.enum(["up", "down"]).optional(),
  }),
};

export const reportAgent = defineAgent({
  name: "report",
  runMetadata: reportRunMetadata,
  // ...
});

Both schemas describe complete objects and must accept {}. Their output must be strict JSON, be accepted as subsequent input to the same schema, and remain unchanged when parsed again. PAI verifies this canonical/idempotent contract and uses the parsed {} output, including defaults and canonicalizing transforms, as each run's initial value. Default factories are evaluated independently when each run is persisted; registration-time parsing validates the schema but does not cache one value for later runs. Input/output-changing transforms such as z.string().transform(Number) are rejected when the declared input excludes the transform's output, because persisted output is the input to future full-object validation. Canonical coercions whose output is also valid input can be used when parsing them again produces the same value. The inferred private type is available only to trusted server surfaces. The public type flows into client-safe entries in ThreadState.runs, React controllers, and generated client contracts.

Lifecycle and trusted runtime writers accept sparse top-level patches, shallow merge them into the current bag, and validate the complete merged object before committing. Put secrets and server coordination in private; put only values safe for ordinary clients in public. Public means client-readable, not client-writable. Keep the raw schema pair as a named export and pass the same object to both defineAgent({ runMetadata }) and metadata-aware defineCommand({ runMetadata }) calls. Both definition boundaries validate the schemas, and agent registration rejects a command bound to a different object.

Thread metadata uses the same raw schema-pair and canonical-validation rules:

export const assistantThreadMetadata = {
  private: z.object({ contextCursor: z.string().optional() }),
  public: z.object({ category: z.string().optional() }),
};

export const assistantAgent = defineAgent({
  name: "assistant",
  threadMetadata: assistantThreadMetadata,
  // ...
});

Private thread metadata is available to lifecycle hooks and trusted runtime code but is omitted from thread heads, lists, ordinary client state, and realtime events. Public thread metadata appears as the ordinary metadata field and remains server-written. Full-object schema validation applies to every merge and replacement.

ModelSelection

import type { JSONValue, LanguageModel, LanguageModelCallOptions } from "ai";

type ModelSettings = Pick<
  LanguageModelCallOptions,
  | "maxOutputTokens"
  | "temperature"
  | "topP"
  | "topK"
  | "presencePenalty"
  | "frequencyPenalty"
  | "stopSequences"
  | "seed"
  | "reasoning"
>;

type ModelProviderOptions = Record<
  string,
  Record<string, JSONValue | undefined>
>;

type ModelSelection = {
  model: LanguageModel;
  modelSettings?: ModelSettings;
  providerOptions?: ModelProviderOptions;
};

modelSettings groups the portable generation controls that PAI spreads into the top level of the AI SDK streamText() call, including portable reasoning effort. PAI uses the SDK's generation-only LanguageModelCallOptions type; abortSignal, retries, timeouts, and headers remain separate transport and runtime policy. The explicit Pick prevents an SDK addition from silently widening this boundary, while the runtime projects the same allowlist so a structurally wider object cannot forward transport controls. A type test pins the approved fields' exact value types to the installed SDK version.

ModelProviderOptions matches the AI SDK's JSON-compatible provider-options envelope. The selected LanguageModel intentionally does not retain a concrete provider-options type, so use the provider package's exported type with satisfies when you want exact keys and values checked:

import type { OpenAILanguageModelResponsesOptions } from "@ai-sdk/openai";

const selection: ModelSelection = {
  model: openai("gpt-5.2"),
  providerOptions: {
    openai: {
      reasoningEffort: "medium",
    } satisfies OpenAILanguageModelResponsesOptions,
  },
};

The main model resolver runs at most once after prepareRun continues into the primary model loop. Its selection becomes the default for every model step in that admitted execution. If prepareRun supplies a model, PAI skips the root resolver. Use prepareModelStep for per-step model routing. A resumed execution resolves the selection again only if it continues into the primary model loop.

If prepareRun returns a synthetic assistant response while transformStepStream is configured, PAI also resolves the root model to build the complete request supplied to that transform. This does not call the model provider.

Thread-title and reasoning-summary model resolvers still return a bare LanguageModel. Their default generators may reuse the selected language model, but they do not inherit main-run modelSettings or providerOptions.

type ThreadTitleConfig = {
  /** Generate at most once, or refresh generated titles over time. */
  generate?: "once" | "refresh";

  /** Refresh thresholds used when `generate` is `"refresh"`. */
  refresh?: {
    afterMessages?: number;
    afterRuns?: number;
    minAgeMs?: number;
  };

  /** Which existing titles automatic generation may replace. Defaults to `"empty"` for one-shot generation and `"generated"` for refresh generation. */
  overwrite?: "empty" | "generated" | "always";

  /** Model used to generate titles. Defaults to the selected language model. */
  model?:
    | LanguageModel
    | ((input: AgentBehaviorInput) => LanguageModel | Promise<LanguageModel>);

  /** Instructions for the title-generation call. Defaults to a concise-title prompt. */
  instructions?:
    | Instructions
    | ((input: ThreadTitleGeneratorInput) => Instructions | Promise<Instructions>);

  /** Custom title generator. Called only when the configured schedule is due. */
  generator?: (input: ThreadTitleGeneratorInput) => string | null | undefined | Promise<string | null | undefined>;
};

threadTitles is disabled unless configured. Set threadTitles: true for the default one-shot generator, or pass a config object for custom generation and refresh policy. Generated titles are persisted as first-class thread title records and appear in ThreadState.thread.title and ThreadSummary.title.

Manual renames through thread.rename(title) or client.threads.rename(threadId, title) are stored with source: "manual" and are not overwritten by automatic generation unless overwrite: "always" is configured. By default, automatic generation only fills empty titles.

type ReasoningSummaryConfig = {
  /** Model used by the default generator. Defaults to the selected language model. */
  model?:
    | LanguageModel
    | ((input: AgentBehaviorInput) => LanguageModel | Promise<LanguageModel>);

  /** Instructions for the default summary-generation call. */
  instructions?:
    | Instructions
    | ((input: ReasoningSummaryGeneratorInput) => Instructions | Promise<Instructions>);

  /** Custom generator. Replaces the default LLM summarization. */
  generator?: (
    input: ReasoningSummaryGeneratorInput,
  ) => string | null | undefined | Promise<string | null | undefined>;
};
type ReasoningSummaryGeneratorInput = {
  runtimeContext: unknown;
  thread: LifecycleThread;
  runId: RunId;
  threadId: ThreadId;
  messageId: MessageId;
  /** Zero-based ordinal among reasoning parts in this model-step message. */
  reasoningOrdinal: number;
  operation: "send" | "trigger" | "regenerate" | "resume";
  startedAt: string;
  /** Complete text of the reasoning part being summarized. */
  reasoning: string;
  signal: AbortSignal;
};

reasoningSummary is disabled unless configured. Set reasoningSummary: true for the default summarizer, or pass a config object with a custom model, instructions, or generator. PAI starts summary generation as soon as a reasoning part closes, without blocking the rest of the model stream. The generated label is retained with that reasoning part and appears as PaiReasoningPart.summary through live PaiMessage updates and refreshed snapshots.

PAI uses Zod schemas for validation, TypeScript inference, manifests, and optional tooling. The public API does not accept other schema libraries.

type AgentBehaviorInput<
  TContract extends AgentContract = AgentContract,
  TRuntimeContext = unknown,
> = {
  /** Runtime-only value returned by the agent's `runtimeContext` builder. */
  runtimeContext: TRuntimeContext;

  /**
   * Trusted caller resolved by the server boundary. The storage scope and
   * actor key are derived from it.
   */
  identity: IdentityOf<TContract>;

  /** Parsed Zod output from the request's `clientData` value. */
  clientData: ClientDataOf<TContract>;

  /** Thread this run belongs to, for per-conversation behavior and context. */
  threadId: ThreadId;

  /** Aborts when the request, run, or server shutdown is cancelled. */
  signal: AbortSignal;
};

identity is trusted server-resolved state. clientData is serializable request metadata from the client and must not be used as access-control input. threadId is the thread the run belongs to, but it can repeat in different partitions; resolvers and the runtimeContext builder must combine it with trusted identity when keying resources.

Tools

Most agents use static tools:

defineAgent({
  tools: {
    readDocument,
    updateDocument,
  },
});

Use a registration config when availability or tool context differs by agent:

defineAgent({
  tools: {
    getRecentActivity: {
      tool: getRecentActivity,
      enabled: ({ runtimeContext }) =>
        runtimeContext.permissions.canReadActivity,
      mapContext: ({ runtimeContext }) => ({
        userId: runtimeContext.user.id,
      }),
    },
  },
});
type AgentToolConfig<
  TRuntimeContext = unknown,
  TTool extends ToolDefinition<any, any, any> = ToolDefinition<any, any, any>,
  TContract extends AgentContract = AgentContract,
> = {
  tool: TTool;
  enabled?:
    | boolean
    | ((input: AgentBehaviorInput<TContract, TRuntimeContext>) =>
        boolean | Promise<boolean>);
  onOutput?: AgentToolOutputObserver<TRuntimeContext, TTool, TContract>;
} & ([TRuntimeContext] extends [ToolContextOf<TTool>]
  ? { mapContext?: AgentToolContextMapper<TRuntimeContext, TTool, TContract> }
  : { mapContext: AgentToolContextMapper<TRuntimeContext, TTool, TContract> });

type MappedAgentToolConfig<
  TRuntimeContext,
  TTool extends ToolDefinition<any, any, any>,
  TContract extends AgentContract,
> = {
  tool: TTool;
  enabled?: AgentToolEnabled<TRuntimeContext, TContract>;
  onOutput?: AgentToolOutputObserver<TRuntimeContext, TTool, TContract>;
  mapContext: AgentToolContextMapper<TRuntimeContext, TTool, TContract>;
};

type AgentToolOutputObserver<TRuntimeContext, TTool, TContract> = (
  input: AgentBehaviorInput<TContract, TRuntimeContext> & {
    input: ToolInputOf<TTool>;
    output: ToolOutputOf<TTool>;
    tool: { name: string };
    call: { toolCallId: string };
  },
) => void | Promise<void>;

type AgentToolContextMapper<TRuntimeContext, TTool, TContract> = (
  input: AgentBehaviorInput<TContract, TRuntimeContext>,
) => ToolContextOf<TTool> | Promise<ToolContextOf<TTool>>;

type ToolContextOf<TTool extends ToolDefinition<any, any, any>> =
  TTool extends ToolDefinition<any, infer TToolContext, any>
    ? TToolContext
    : unknown;

type ToolInputOf<TTool extends ToolDefinition<any, any, any>> =
  TTool extends ToolDefinition<infer TContract, any, any>
    ? TContract["input"]
    : unknown;

type ToolOutputOf<TTool extends ToolDefinition<any, any, any>> =
  TTool extends ToolDefinition<infer TContract, any, any>
    ? TContract["output"]
    : unknown;

enabled is evaluated from the current agent runtime context for every model step and again when a pending tool action is resumed. Disabled tools are not advertised to the model and cannot execute, including through stale or forged tool calls. The static agent contract and manifest still include every declared tool because they describe the agent's complete possible surface.

Availability is a coarse model-facing gate, not authorization for a particular call. Keep resource-level permission checks inside execute, derive them from the trusted identity, and never authorize from clientData.

mapContext maps agent runtime context only. Capability declarations pass through configured tools untouched — an AgentToolConfig contributes its inner tool's capabilities to the agent as if it were registered directly. A tool can be registered directly only when the agent runtime context is assignable to its declared context; otherwise TypeScript requires mapContext.

onOutput observes this tool's own finished calls, so an agent can keep state from a result that the portable tool has no business knowing about. It takes the same AgentBehaviorInput as enabled and mapContext, plus input and output typed from the tool's own schemas, the name this agent registered it under, and the call id. lifecycle.afterToolResult sees every tool's result as name: string with output: unknown; onOutput is the typed, per-tool alternative.

It runs once per call, on the settled and validated output — after afterToolResult has had its say — including a resumed suspension, a client-submitted result, a skipped call's synthetic output, and a failure afterToolResult converted into an output. A failed call does not reach it, and neither does an output afterToolResult turned into an error. It cannot rewrite the result: the return value is ignored. Throwing fails the run, reported through onRuntimeError and redacted from the client. PAI awaits it before the output reaches the model, and its side effects are not transactional with the tool result, which is persisted after it returns.

Tool renderers do not receive backend context. Context mapping is for backend tool execution. Values needed by the UI should be exposed through tool input, output, suspend payloads, or tool data.

Server-owned tools are declared through the tools map. If your app discovers backend tools dynamically, resolve that discovery before constructing the agent or construct an agent per resolved tool set.

AI SDK provider-defined tools use the separate providerTools map. PAI sends them to the model provider without treating them as PAI backend or client executions, while still carrying their AI SDK input/output types into InferAgentContract for typed native tool parts and React renderers.

defineAgent({
  // ...
  providerTools: {
    google_search: vertex.tools.googleSearch({}),
  },
});

Use the exact registration name documented by the provider. Google Search, for example, must be registered as google_search and commonly emits AI SDK source parts rather than a synthetic tool call/result. Those sources remain native typed message parts.

PAI preserves AI SDK provider-tool parts, but provider-specific request and conversation-history encoding is implemented by the installed provider package. Support can vary by provider, tool, and version; registration in PAI does not add capabilities that the provider does not support.

Provider tools are declared statically because PAI must refine retained native tool parts against the current input and output schemas. Lifecycle model patches may select a subset by reusing these exact definitions, but cannot introduce or replace provider-tool schemas per run or model call. Removing or incompatibly changing a declared provider tool requires resetting retained development threads during the current direct-reset phase.

Frontend-defined client tools are different: they are declared by React provider/session clientTools config or mounted useClientTool({ ... }) hooks, then included in request snapshots as serialized client-tool definitions.

Lifecycle

lifecycle is the advanced extension point for changing how a run starts, what the model sees, wrapping model calls, transforming canonical step output, and applying tool-call policy.

Reusable helpers such as RAG, prompt-cache provider options, canonical redaction, model fallback, and context compaction should be implemented as lifecycle helpers. Normal app code does not need lifecycle hooks.

Lifecycle helpers compose left to right:

import { composeLifecycle } from "@pai/core";

export const agent = defineAgent({
  name: "report",
  lifecycle: composeLifecycle(
    withWorkspaceRag(),
    redactSecrets(),
    {
      prepareRun({ runtimeContext }) {
        if (!runtimeContext.quota.allowed) {
          return {
            action: "respond",
            outcome: "blocked",
            reason: "quota_exceeded",
            response: {
              parts: [{ type: "text", text: "You have reached your usage limit." }],
            },
          };
        }
      },
    },
  ),
});

See Lifecycle for the full type reference and Lifecycle Hooks for examples.

Use lifecycle for cross-cutting agent behavior. Use tools for model-callable capabilities and runtimeContext for building ordinary agent-wide dependencies for an execution episode. Queueing, leases, wakeups, and cancellation remain runtime behavior, not lifecycle hooks.

Generic middleware, inputProcessors, and outputProcessors are not separate AgentConfig fields. They map into lifecycle phases or client/transport presentation code.

AgentDefinition

type AgentDefinition<
  TContract extends AgentContract = AgentContract,
  _TRuntimeContext = unknown,
  TCapabilities extends CapabilityMap = {},
> = {
  /** Inferred literal agent name. */
  readonly name: AgentName<TContract>;

  /** Resolved version, defaulted by PAI when omitted. */
  readonly version: string;

  /** Full registered contract, including server-only commands. */
  readonly contract: TContract;

  /** Serializable runtime metadata for clients, tooling, and validation. */
  readonly manifest: AgentManifest<TContract>;

  /** Capability requirements aggregated from this agent's operations, keyed by id. */
  readonly capabilities: TCapabilities;
};

The returned AgentDefinition is server-side. Pass it to createAgentRuntime() or a framework adapter such as createPaiHonoReceiver().

capabilities is the id-keyed aggregate of every capability the agent's tools and commands declare. defineAgent computes it by folding both maps — operation-local aliases are remapped to capability ids and configured tool entries contribute their inner tool's declarations — and throws CapabilityBindingError when one id is declared through distinct capability objects. createAgentRuntime uses the aggregate to require a typed binding record; see Runtime.

type InferAgentCapabilities<
  TTools extends Record<string, unknown>,
  TCommands extends Record<string, CommandDefinition>,
> = /* id-keyed fold over both operation maps */;

type AgentCapabilitiesOf<TAgent> =
  TAgent extends AgentDefinition<any, any, infer TCapabilities>
    ? TCapabilities
    : {};

InferAgentCapabilities is the type-level counterpart of that fold; defineAgent returns AgentDefinition<..., ..., InferAgentCapabilities<TTools, TCommands>>. Use AgentCapabilitiesOf to read the requirement map back off a finalized agent.

export const runtime = createAgentRuntime({
  agent: assistantAgent,
  scopeKey: (identity) => identity.workspaceId,
});

The contract field is the type-level public shape inferred from the config. Application code usually uses it through InferAgentContract.

export type AssistantAgentContract = InferAgentContract<typeof assistantAgent>;

A TypeScript client usually imports the app-level InferPaiContract type with import type. No generated files are required for the primary TypeScript path.

export const pai = createPai({
  agents: { main: assistantAgent },
  scopeKey: (identity) => identity.workspaceId,
});
export type AssistantPai = InferPaiContract<typeof pai>;
import type { AssistantPai } from "../../server/pai";

const paiClient = createPaiHttpClient<AssistantPai>({
  url: "http://localhost:3001/api/pai",
});

AgentContract

type AgentContract = {
  /** Public agent name. */
  name: string;

  /** Optional public agent version. */
  version?: string;

  /** Parsed output of the agent's Zod `identity` schema. */
  identity: unknown;

  /** Parsed output of the agent's Zod `clientData` schema. */
  clientData: unknown;

  /** Typed server-only and client-readable metadata carried by each run. */
  runMetadata?: {
    private: JsonObject;
    public: JsonObject;
  };

  /** Typed server-only and client-readable metadata carried by each thread. */
  threadMetadata?: {
    private: JsonObject;
    public: JsonObject;
  };

  /** Typed server-only and client-readable metadata carried by each message. */
  messageMetadata?: {
    private: JsonObject;
    public: JsonObject;
  };

  /** Application-authored native AI SDK data parts keyed without `data-`. */
  uiData: Record<string, JsonValue>;

  /** Concrete agent contracts replace this with tools keyed by registration name. */
  tools: Record<never, never>;

  /** Concrete agent contracts replace this with commands keyed by registration name. */
  commands: Record<never, never>;
};

AgentContract is a TypeScript type used by clients, React hooks, tests, and provider conformance helpers. It is not the runtime agent implementation.

AgentManifest

type AgentManifest<TContract extends AgentContract = AgentContract> = {
  /** Agent name from the contract. */
  name: TContract["name"];

  /** Optional application-defined agent version. */
  version?: TContract["version"];

  /** Registered model-visible tools keyed by registration name. */
  tools: Record<string, ToolManifestEntry>;

  /** Commands intentionally exposed to ordinary clients. */
  commands?: Record<string, CommandManifestEntry>;
};

The manifest is serializable discovery metadata for registered tools and client-exposed commands. Compile-time payload types remain in AgentContract, and the agent's Zod schemas remain on the trusted server.

The manifest does not contain executable functions, JSON Schema projections, or a contract hash. It must not contain secrets, database handles, or server-only context.

AgentRuntimeContextBuilder

type AgentRuntimeContextBuilder<
  TRuntimeContext = unknown,
  TContract extends AgentContract = AgentContract,
> = (input: {
  /** Trusted caller resolved by the server boundary. */
  identity: IdentityOf<TContract>;

  /** Parsed client metadata for this request or run. */
  clientData: ClientDataOf<TContract>;

  /** Thread this run belongs to, for per-conversation context. */
  threadId: ThreadId;

  /** Aborts when the active operation is cancelled. */
  signal: AbortSignal;
}) => TRuntimeContext | Promise<TRuntimeContext>;

The runtime-context builder turns trusted request state into agent-wide, runtime-only state. It receives threadId and trusted scope, so scope-qualified per-conversation resources (a sandbox key, a scratch namespace) can be derived here and read from runtimeContext by resolvers and lifecycle hooks.

runtimeContext: async ({ identity, threadId, signal }) => ({
  workspace: await loadWorkspace(identity.workspaceId, { signal }),
  sandboxKey: JSON.stringify(["my-app", "sandbox", identity.workspaceId, threadId]),
  logger,
})

runtimeContext can contain services, functions, database clients, model selectors, loggers, and loaded records. It is not serialized to clients or persisted as thread state. Tool registrations may pass a narrower value through mapContext; tool execution then reads that value from ctx.context.

InferAgentContract

import type { z } from "zod";

type InferAgentContract<TAgentOrConfig> = ClientAgentContract<
  TAgentOrConfig extends AgentDefinition<infer TContract>
    ? TContract
    : TAgentOrConfig extends AgentConfig
      ? InferRegisteredAgentConfigContract<TAgentOrConfig>
      : never
>;

type InferRegisteredAgentConfigContract<TConfig extends AgentConfig> = {
  /** Literal agent name from `defineAgent({ name })`. */
  name: TConfig["name"];

  /** Literal version when supplied. */
  version?: TConfig["version"] extends string ? TConfig["version"] : string;

  /** Parsed output of the Zod `identity` schema. */
  identity: TConfig["identity"] extends z.ZodType
    ? z.output<TConfig["identity"]>
    : unknown;

  /** Parsed output of the Zod `clientData` schema. */
  clientData: TConfig["clientData"] extends z.ZodType
    ? z.output<TConfig["clientData"]>
    : unknown;

  /** Private/public run metadata inferred from the registered schema pair. */
  runMetadata?: RunMetadataContractOf<TConfig["runMetadata"]>;

  /** Private/public thread metadata inferred from the registered schema pair. */
  threadMetadata?: ThreadMetadataContractOf<TConfig["threadMetadata"]>;

  /** Private/public message metadata inferred from the registered schema pair. */
  messageMetadata?: MessageMetadataContractOf<TConfig["messageMetadata"]>;

  /** Application data payloads inferred from `uiDataSchemas`. */
  uiData: TConfig["uiDataSchemas"] extends Record<string, z.ZodType>
    ? {
        [TName in keyof TConfig["uiDataSchemas"]]: z.output<
          TConfig["uiDataSchemas"][TName]
        >;
      }
    : {};

  /** Tool contracts inferred from `tools`. */
  tools: InferAgentToolContracts<TConfig["tools"]>;

  /** Server command contracts inferred from `commands`. */
  commands: InferAgentCommands<TConfig["commands"]>;
};

Use InferAgentContract to expose the public type shape of an agent without exposing the server runtime implementation.

export const assistantAgent = defineAgent({ ... });
export type AssistantAgentContract = InferAgentContract<typeof assistantAgent>;

The public projection omits server-only commands and erases the private run and thread metadata shapes to JsonObject. Backend code that needs every registered command and both private metadata types can use InferRegisteredAgentContract<typeof agent> or typeof agent.contract; runtime inference normally makes this explicit type unnecessary.

On this page