Agents (Beta)
Beta / preview API. Agents are not yet stable. Server APIs come from
genkit/beta; the browser client comes fromgenkit/beta/client. Import paths and signatures may change. Always usegenkit/beta, notgenkit, for agents.Requires
genkit>= 1.39.0.
An agent is a persistent, multi-turn conversation primitive built on top of
prompts + tools. Compared to a bare ai.generate/ai.definePrompt loop, an
agent adds:
- Sessions: multi-turn history tracked as immutable snapshots.
- State: typed session state (messages + custom data + artifacts).
- Interrupts: human-in-the-loop pause/resume.
- Branching: fork a conversation from any snapshot.
- Detaching: run a turn in the background and poll for the result.
Progressive disclosure — read the file for the level you need:
- This file: defining an agent, serving it, and client-managed state (no store).
- Sessions & persistence:
SessionStore,InMemorySessionStore,FileSessionStore,FirestoreSessionStore. - Human-in-the-loop / interrupts: pausing for approval/input and resuming.
- Branching: forking a conversation from a snapshot.
- Background agents: detaching long-running turns and polling.
- Working with state: typed custom session state and client auto-sync.
- Artifacts: producing/reading named deliverables.
- Multi-agent orchestration: delegating to sub-agents.
- Advanced custom agents:
defineCustomAgentfor full turn control. - Deploying agents: serving multiple agents over HTTP, CORS, web UI, other frameworks.
Setup
// genkit.ts — note the `genkit/beta` import (required for agents)
import { genkit } from 'genkit/beta';
import { googleAI } from '@genkit-ai/google-genai';
export const ai = genkit({
plugins: [googleAI()],
model: googleAI.model('gemini-flash-latest'),
});
Define an agent
ai.defineAgent combines prompt + tool config + (optional) session store into a
single registered action.
import { z } from 'genkit';
import { ai } from './genkit.js';
const getWeather = ai.defineTool(
{
name: 'getWeather',
description: 'Look up the current weather for a city.',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ tempC: z.number(), summary: z.string() }),
},
async ({ city }) => ({ tempC: 21, summary: 'Sunny' })
);
export const weatherAgent = ai.defineAgent({
name: 'weatherAgent',
system:
'You are a helpful weather assistant. Use the getWeather tool. Be concise.',
tools: [getWeather],
// `store` is optional. Omit it for client-managed state (see below).
});
Common defineAgent options:
name(required): action name.system/prompt/ dotprompt fields: same asdefinePrompt.tools: tools and interrupts available to the agent.model: override the default model.store: aSessionStorefor server-side persistence. See sessions.stateSchema: az.ZodType<State>describing custom session state. When set,Stateis inferred and validated at load time.input/inputSchema: input variables for the prompt template.
Agents and middleware go hand in hand
Agents and middleware are built for each other: the use: [...]
array is where you layer in sophisticated behavior without writing it yourself.
Most agent capabilities — sub-agent delegation, artifacts, filesystem access,
skill loading, tool approval, retries — are just middleware you drop in.
For example, a full coding assistant is mostly configuration:
import { filesystem, retry, skills, toolApproval } from '@genkit-ai/middleware';
import { FileSessionStore } from 'genkit/beta';
import { ai } from './genkit.js';
export const codingAgent = ai.defineAgent({
name: 'codingAgent',
system: 'You are an expert AI coding assistant working in a sandboxed workspace.',
tools: [runShell, askUser], // your own custom tools/interrupts
use: [
// Require user approval (interrupt) before risky tools; reads auto-approved.
// Order matters: keep toolApproval before filesystem.
toolApproval({
approved: ['list_files', 'read_file', 'use_skill', 'run_shell', 'ask_user'],
}),
// list_files / read_file / write_file / search_and_replace, sandboxed.
filesystem({ rootDirectory: WORKSPACE_DIR, allowWriteAccess: true }),
// Load coding conventions on demand via a use_skill tool.
skills({ skillPaths: [SKILLS_DIR] }),
// Automatic retry on transient model errors.
retry(),
],
store: new FileSessionStore('./.snapshots-coding'), // needed for tool approval
maxTurns: 30,
});
That single use array gives the agent filesystem tools, an on-demand skills
library, human-in-the-loop approval for writes, and retries — each is one line.
See using middleware for the full catalog and
building custom middleware to write your own.
Chat with an agent (server-side)
agent.chat() opens a conversation. A single chat carries state forward
automatically across turns.
const chat = weatherAgent.chat();
// Non-streaming turn:
const res = await chat.send('Weather in Tokyo?');
console.log(res.text);
console.log(res.snapshotId); // immutable checkpoint id for this turn
// Follow-up turn — history is carried automatically:
const res2 = await chat.send('What about Paris?');
// Streaming turn:
const turn = chat.sendStream('And London?');
for await (const chunk of turn.stream) {
process.stdout.write(chunk.text ?? '');
}
const final = await turn.response;
Verify an agent from the CLI (flow:run)
genkit flow:run only runs flows, not agents, so you can't flow:run an
agent directly. To exercise an agent from the CLI (e.g. a quick, self-terminating
check), wrap one turn in a throwaway flow and run that:
import { z } from 'genkit';
import { weatherAgent } from './weather-agent.js';
import { ai } from './genkit.js';
export const tryWeatherAgent = ai.defineFlow(
{ name: 'tryWeatherAgent', inputSchema: z.string(), outputSchema: z.string() },
async (message) => (await weatherAgent.chat().send(message)).text
);
// genkit flow:run tryWeatherAgent '"Weather in Tokyo?"' -- npx tsx src/index.ts
Serve an agent over HTTP
Use expressHandler from @genkit-ai/express. Optionally expose the agent's
companion getSnapshotDataAction (for state lookup/restore) and
abortAgentAction (for background aborts).
import { expressHandler } from '@genkit-ai/express';
import express from 'express';
import { weatherAgent } from './weather-agent.js';
const app = express();
app.use(express.json());
// Main turn endpoint:
app.post('/api/weatherAgent', expressHandler(weatherAgent));
// Optional companions (needed for snapshot restore / branching / background):
app.post(
'/api/weatherAgent/getSnapshot',
expressHandler(weatherAgent.getSnapshotDataAction)
);
app.post(
'/api/weatherAgent/abort',
expressHandler(weatherAgent.abortAgentAction)
);
app.listen(8080);
For serving multiple agents, CORS/streaming headers for browser clients, a static web UI, and other host frameworks (Next.js, Firebase), see Deploying agents.
Consume an agent from a client (remoteAgent)
The browser/Node client lives in genkit/beta/client. remoteAgent returns a
typed HTTP client; getSnapshotUrl/abortUrl default to ${url}/getSnapshot
and ${url}/abort.
import { remoteAgent, AgentError } from 'genkit/beta/client';
const weather = remoteAgent({ url: 'http://localhost:8080/api/weatherAgent' });
const chat = weather.chat();
const turn = chat.sendStream('Weather in Tokyo?');
for await (const chunk of turn.stream) {
process.stdout.write(chunk.text ?? '');
}
const res = await turn.response;
console.log(res.snapshotId, chat.snapshotId, chat.state);
// Multi-turn — the client carries state forward automatically:
await chat.send('What about Paris?');
// Errors surface as AgentError with an HTTP-ish status:
try {
await remoteAgent({ url: '/api/nope' }).chat().send('hi');
} catch (err) {
if (err instanceof AgentError) console.log(err.status);
}
Client-managed state (no server store)
If the agent has no store, the server is fully stateless and the session
state blob (messages + custom + artifacts) is owned by the caller. The
remoteAgent client tracks it and round-trips it on every turn automatically —
no SessionStore, no snapshot ids to manage.
// Server: no `store` → stateless. Client owns the state blob.
export const weatherAgentStateless = ai.defineAgent({
name: 'weatherAgentStateless',
system: 'You are a helpful weather assistant. Use getWeather. Be concise.',
tools: [getWeather],
});
// Client: reuse one `chat` and the state threads automatically.
import { remoteAgent, type AgentChat } from 'genkit/beta/client';
const agent = remoteAgent({ url: '/api/weatherAgentStateless' });
const chat: AgentChat = agent.chat();
await chat.send('Weather in London?');
await chat.send('Is it sunny in Tokyo?'); // remembers prior turns
// The full state blob is available after each turn:
const res = await chat.send('And Paris?');
console.log(JSON.stringify(res.raw.state, null, 2));
If you call the stateless agent directly (e.g. from a flow) instead of via
remoteAgent, you must round-trip the state yourself:
async function turn(state: unknown | undefined, text: string) {
// Resume from prior state, or start fresh on the first turn.
const chat = weatherAgentStateless.chat(state ? { state } : undefined);
const res = await chat.send(text);
// Return the updated state so the caller can pass it back next turn.
return { state: res.raw.state, text: res.text };
}
Use client-managed state when you don't want to run server-side storage (e.g. the client persists history itself). Use a session store when the server should own history, or when you need branching or background execution. (Interrupts work either way.)