TypeScript

End-to-end type inference from server to client.

Better Agent keeps your server and client in sync through TypeScript. Agent names, run context, structured output, and client tool handlers are inferred from your app definition.

Use a direct app type import in the same project, or generate a portable type with the CLI when your client lives in a separate package.

What You Get

Each part of your app definition becomes a typed contract on the client:

DefinitionClient Type
agent.nameString literal union for the agent parameter
agent.contextSchemaTyped context field on run() and stream() input
agent.outputSchemaTyped structured field on the run result
Client tools (.client())Typed toolHandlers map with handler input inferred from the tool's schema
model.capsCapability-gated fields. modalities, outputSchema, and similar fields only appear when the model supports them

Same Project

When your client and server share the same TypeScript project, import the app type directly.

server.ts
import { defineAgent, betterAgent } from "@better-agent/core";
import { z } from "zod";
import { openai } from "./openai";

const supportAgent = defineAgent({
  name: "support",
  model: openai.model("gpt-4o"),
  contextSchema: z.object({
    userId: z.string(),
    plan: z.enum(["free", "pro"]),
  }),
  outputSchema: {
    schema: z.object({
      reply: z.string(),
      sentiment: z.enum(["positive", "negative", "neutral"]),
    }),
  },
});

export const app = betterAgent({
  agents: [supportAgent],
});
client.ts
import { createClient } from "@better-agent/client";
import type { app } from "./server";

const client = createClient<typeof app>({
  baseURL: "/api",
  secret: "dev-secret",
});

const result = await client.run("support", {
  input: "I need help",
  context: { userId: "user_1", plan: "pro" },
});

result.structured.reply;
result.structured.sentiment;

No CLI or codegen step is needed. TypeScript infers everything from the import.

Separate Packages

When your client and server live in different packages or repos, use the CLI to generate a portable .d.ts file.

Terminal
npx @better-agent/cli generate type --config ./server.ts

This produces a file like:

better-agent.types.d.ts
export type BAClientApp = {
    config: {
        agents: readonly [{
            name: "support";
            model: {
                caps: {
                    structured_output: true;
                    multi_turn: true;
                };
            };
            contextSchema: {
                userId: string;
                plan: "free" | "pro";
            };
            outputSchema: {
                schema: {
                    reply: string;
                    sentiment: "positive" | "negative" | "neutral";
                };
            };
        }];
    };
};

Then import it in the client:

client.ts
import { createClient } from "@better-agent/client";
import type { BAClientApp } from "./better-agent.types";

const client = createClient<BAClientApp>({
  baseURL: "https://api.example.com",
  secret: process.env.BETTER_AGENT_SECRET!,
});

See CLI for the full command reference.

How Inference Works

The chain starts at betterAgent() and ends at createClient().

defineAgent({ name, model, contextSchema, outputSchema, tools })
  -> betterAgent({ agents: [...] })
    -> typeof app
      -> createClient<typeof app>({ ... })

For untyped clients, createClient({ ... }) without a type parameter, all fields fall back to string and unknown.

What Typegen Needs

The CLI uses the TypeScript compiler to extract a portable structural type from your app definition. This works reliably for static, literal-shaped configs.

Supported patterns:

  • Inline agents and tools array literals
  • Direct constant references, such as const agent = defineAgent({...})
  • defineTool(...).client() calls
  • Object literal schemas (contextSchema, outputSchema.schema, tool schemas)
  • Typed model references when model.caps is visible to TypeScript

Unsupported patterns:

  • Spread composition, such as [...sharedAgents] or [...sharedTools]
  • Computed arrays, such as getAgents() or tools.concat(otherTools)
  • Opaque schema instances that cannot be rendered structurally
  • Dynamic config builders where literal names or schemas are widened away

When the CLI cannot preserve type semantics, it fails with a validation error instead of emitting a misleading type. The error message includes the path to the unsupported expression.