Skip to content

Sessions and state

In Genkit, agent state includes message history, custom application state, artifacts, session identity, and snapshot lineage. Choose the state strategy before building the client because it determines who owns continuity between turns.

Genkit agents can keep continuity in two ways.

Server-managed state means the agent has a store. The server persists messages, custom state, artifacts, and snapshot metadata. Clients continue by sending a sessionId or snapshotId. Use this mode for persistent chat apps, shared devices, background execution, branching from saved points, or any workflow where clients should not carry the full conversation payload.

Client-managed state means the agent has no store. The server returns the full SessionState, and the client sends that state back on the next turn. Use this mode when your app already owns persistence, needs stateless server deployments, wants to encrypt conversation state outside Genkit, or has short sessions where carrying the full state is acceptable.

Prefer server-managed state when you are unsure. It gives you snapshots, loadChat(), background work, and smaller client payloads. Prefer client-managed state when infrastructure control matters more than built-in persistence.

In both modes, the AgentChat object tracks the next-turn values for you. A server-managed chat tracks snapshotId and sessionId. A client-managed chat tracks full SessionState.

The full state object has three user-visible pieces:

type SessionState<S> = {
custom?: S;
messages?: MessageData[];
artifacts?: Artifact[];
};
  • custom is your typed application state. Use it for compact data that the agent or UI needs to make decisions across turns, such as workflow status, task lists, selected entities, preferences, draft metadata, or progress indicators.
  • messages is conversation history. The runtime updates it as user and model messages are added. You usually read messages rather than manually rewriting them, except in custom orchestration.
  • artifacts is a list of generated outputs, such as files, reports, plans, code patches, media references, or structured documents. Use artifacts when the value is an output the user may inspect, download, reuse, or version independently.

Custom state and artifacts both live in session state, so choose by role:

  • Use custom state for the compact control and UI data that drives the next turn, such as workflow status, task lists, selected entities, preferences, or progress. It rides in every snapshot and client payload, so keep it small.
  • Use artifacts for generated outputs the user may inspect, download, reuse, or version independently, such as reports, files, patches, itineraries, or media.

Do not put large generated documents into custom just because they are JSON; make them artifacts.

Tools and custom agents can update custom state through the active session. Use updateCustom(fn) so Genkit can stream patches and keep the client-side chat state current.

const session = ai.currentSession<TaskState>();
const title = 'Buy milk';
session.updateCustom((state) => {
const next = state ?? { tasks: [], nextId: 1 };
return {
...next,
tasks: [...next.tasks, { id: next.nextId, title, done: false }],
nextId: next.nextId + 1,
};
});

Treat custom state updates as application state transitions. Return a new value from the updater, keep it serializable, and validate it with stateSchema when you need stronger guarantees at load time.

Add a store when the server should own history and snapshots:

import { FileSessionStore, genkit } from 'genkit/beta';
const store = new FileSessionStore<WeatherState>('./.genkit/snapshots/weather');
const agent = ai.defineAgent({
name: 'weatherAgent',
system: 'Answer weather questions.',
stateSchema: WeatherStateSchema,
store,
});

Every successful turn writes a completed snapshot. The snapshot includes the session ID, parent snapshot ID, finish reason, state, timestamps, and status. Failed turns return the last-good state or snapshot instead of making partial state the normal resume point.

For store options and custom store implementation guidance, see Session stores.

Read a snapshot by ID or read the latest snapshot for a session:

const exact = await agent.getSnapshot({ snapshotId });
const latest = await agent.getSnapshot({ sessionId });

You can pass a snapshot ID string as shorthand:

const snapshot = await agent.getSnapshot(snapshotId);

Snapshot statuses are:

StatusMeaning
pendingA detached background invocation is still running.
completedThe snapshot captures a settled, resumable state.
failedThe invocation failed. Error details are stored on the snapshot.
abortedThe detached invocation was canceled.
expiredA pending snapshot heartbeat went stale, so the background worker is presumed dead.

Only completed snapshots are valid resume points. Other statuses are useful for inspection, polling, and recovery UI.

Use sessionId when the user wants the latest state in a conversation:

const chat = agent.chat({ sessionId: 'support-ticket-123' });
await chat.send('Continue where we left off.');

Use snapshotId when the user wants a specific point in history:

const branch = agent.chat({ snapshotId: approvedPlanSnapshotId });
await branch.send('Revise this plan for a smaller budget.');

When both values are supplied, the snapshot chooses the resume point and the session ID validates ownership.

Without a store, the server returns the whole state and the client sends it back:

const chat = agent.chat({
state: {
custom: { tasks: [], nextId: 1 },
messages: [],
artifacts: [],
},
});
const res = await chat.send('Add buy milk to my list.');
saveState(res.raw.state);

Store res.raw.state wherever your app keeps user session data, then pass it back with chat({ state }) or keep using the same AgentChat instance. Because the client owns the full state, design for payload growth. Long conversations, many artifacts, or large custom objects can make every request heavier.

When custom state changes during a turn, the runtime streams RFC 6902 JSON Patch chunks. AgentChat applies them in order. The resulting custom state appears on chunk.custom and chat.state.

const turn = researchAgent.chat().sendStream('Research electric vehicles.');
for await (const chunk of turn.stream) {
if (chunk.custom?.status) {
renderStatus(chunk.custom.status);
}
}

The first custom patch in each turn is a whole-document replace that rebases the client on the server’s current custom state. Later patches are incremental.

Artifacts are stored as named outputs in session state. Add them from a tool or custom agent through the active session.

const session = ai.currentSession<PlanState>();
session.addArtifacts([
{
name: 'itinerary.json',
parts: [{ text: JSON.stringify(plan) }],
metadata: { contentType: 'application/json' },
},
]);

Artifacts with the same name replace earlier artifacts. Unnamed artifacts are appended. Prefer a named artifact for outputs that should have stable identity, such as itinerary.json, patch.diff, or report.md. See Custom state vs. artifacts for when to use an artifact instead of custom state.

Use clientTransform when raw session state should not leave the server. A state transform shapes snapshots and final state. A chunk transform shapes streamed chunks.

const agent = ai.defineAgent({
name: 'supportAgent',
system: 'Help support agents summarize cases.',
store,
clientTransform: {
state: (state) => ({
...state,
custom: {
...state.custom,
internalNotes: undefined,
},
}),
chunk: (chunk) => chunk,
},
});

Keep state and chunk transforms consistent when they touch the same data. If state redaction changes custom state, the custom patch stream is diffed from the transformed state so clients see a coherent view.