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:
| Definition | Client Type |
|---|---|
agent.name | String literal union for the agent parameter |
agent.contextSchema | Typed context field on run() and stream() input |
agent.outputSchema | Typed structured field on the run result |
Client tools (.client()) | Typed toolHandlers map with handler input inferred from the tool's schema |
model.caps | Capability-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.
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],
});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.
npx @better-agent/cli generate type --config ./server.tsThis produces a file like:
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:
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
agentsandtoolsarray 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.capsis visible to TypeScript
Unsupported patterns:
- Spread composition, such as
[...sharedAgents]or[...sharedTools] - Computed arrays, such as
getAgents()ortools.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.