Agent

An agent is a defineAgent config: name, model, instruction, tools, memory, access, output, and hooks.

import { defineAgent } from "@better-agent/core";
import { openai } from "@better-agent/openai";

const agent = defineAgent({
  name: "support",
  model: openai("gpt-5.5"),
  instruction: "You help resolve customer issues.",
});

The name is a string literal type used to retrieve the agent with app.agent("support").

Instruction

Use a string for static behavior. Use a function for request-aware behavior.

const agent = defineAgent({
  name: "support",
  model: openai("gpt-5.5"),
  instruction: (context) =>
    `You are a support agent for ${context.company}. Speak ${context.language}.`,
  contextSchema: z.object({
    company: z.string(),
    language: z.string(),
  }),
});

The function receives typed context and can be async.

Context schema

contextSchema types the per-request context passed at runtime. The schema can be Zod, any Standard Schema, or plain JSON Schema.

const agent = defineAgent({
  name: "analyst",
  model: openai("gpt-5.5"),
  contextSchema: z.object({
    userId: z.string(),
    plan: z.enum(["free", "pro", "enterprise"]),
  }),
  instruction: (ctx) =>
    `The user is on the ${ctx.plan} plan. Adjust depth accordingly.`,
});

The inferred type flows through instructions, tool execute calls, and hooks. Pass context when running the agent:

await app.agent("analyst").run({
  messages,
  context: { userId: "usr_123", plan: "pro" },
});

Tools

tools can include local defineTool tools, provider tool configs, or a tool source such as mcpTools.

const agent = defineAgent({
  name: "assistant",
  model: openai("gpt-5.5"),
  instruction: "You help users manage their account.",
  tools: [cancelSubscription, updateProfile, stripeTools],
});

Use toolChoice when the model must call a specific tool or mode:

const agent = defineAgent({
  name: "classifier",
  model: openai("gpt-5.5"),
  instruction: "Classify the incoming message.",
  tools: [classifyTool],
  toolChoice: { type: "tool", toolName: "classify" },
});

See Tools for tool types and approvals, and MCP for remote tool servers.

Memory

Memory loads previous thread messages for an agent. Configure it per agent with createMemory, or set it at the app level.

import { defineAgent, createMemory } from "@better-agent/core";

const agent = defineAgent({
  name: "support",
  model: openai("gpt-5.5"),
  instruction: "You help resolve customer issues.",
  memory: createMemory({ lastMessages: 20 }),
});

Set memory: false to opt an agent out of app-level memory.

const stateless = defineAgent({
  name: "classifier",
  model: openai("gpt-5.5"),
  instruction: "Classify the input.",
  memory: false,
});

Use lastMessages to limit loaded history. Use scope to isolate storage by tenant, user, or another derived key. See Memory for setup.

Access control

access controls who can invoke an agent over HTTP.

ValueBehavior
"public"Anyone can call the agent.
"authenticated"Requires a non-null auth context.
(ctx) => booleanCustom function receives { auth, agentName, request }.

When the app has an auth resolver and the agent does not set access, it defaults to "authenticated".

const agent = defineAgent({
  name: "admin-tools",
  model: openai("gpt-5.5"),
  instruction: "You perform admin operations.",
  access: ({ auth }) => auth?.scopes?.includes("admin") ?? false,
});

For app-level auth setup, see Auth.

Structured output

output.schema validates the final response and exposes typed data on result.structured.

const agent = defineAgent({
  name: "extractor",
  model: openai("gpt-5.5"),
  instruction: "Extract structured data from the message.",
  output: {
    schema: z.object({
      sentiment: z.enum(["positive", "negative", "neutral"]),
      topics: z.array(z.string()),
    }),
  },
});

See Structured output for schema formats, runtime overrides, typing, and provider behavior.

Lifecycle hooks

Hooks observe and control the runtime loop at the agent level.

onStep

Runs before each model call. Use it to update messages, filter active tools, or patch state.

const agent = defineAgent({
  name: "support",
  model: openai("gpt-5.5"),
  instruction: "You help resolve customer issues.",
  tools: [refundTool, lookupTool, escalateTool],
  onStep({ stepIndex, setActiveTools }) {
    if (stepIndex === 0) {
      setActiveTools(["lookup"]);
    }
  },
});

setActiveTools restricts which tools the model sees for that step. It does not remove them from the agent.

Other loop controls:

OptionUse
onStepFinishObserve each step result, token usage, and tool call count.
onStateReact to state.set() and state.patch().
stopWhenStop early when a predicate returns true.
maxStepsSet a hard ceiling on loop iterations.
const agent = defineAgent({
  name: "researcher",
  model: openai("gpt-5.5"),
  instruction: "Research the topic thoroughly.",
  maxSteps: 10,
});

For streamed runtime events, see Events.

Running an agent

Register agents in a betterAgent app, then call .run() or .stream() on the handle. See Client for browser usage.

import { betterAgent } from "@better-agent/core";

const app = betterAgent({ agents: [agent] });

const result = await app.agent("support").run({
  messages: [{ role: "user", content: "I need a refund." }],
});

const stream = await app.agent("support").stream({
  messages: [{ role: "user", content: "I need a refund." }],
});

for await (const event of stream.events) {
  console.log(event.type);
}

.run() returns a RunResult. .stream() returns events and a final promise that resolves to a RunResult. Interrupted runs can be resumed. See Human in the Loop.

Type inference

Agent names, context, tools, hooks, and output are inferred from the definition. app.agent("name") returns a typed handle. See TypeScript for the broader typing model.