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 Dart agents can maintain conversation continuity using one of two strategies:

Server-managed state means the agent has a store (e.g. FileSessionStore or InMemorySessionStore). The server persists messages, custom state, and artifacts between turns. Clients continue the chat by sending a sessionId (loads latest state) or snapshotId (loads exact state). Use this for long-running chat applications, multi-turn generators, background detached tasks, or any workflow where client payload size should remain small.

Client-managed state means the agent has no store. The server returns the full SessionState at the end of each turn, and the client must echo that state back on the next turn using chat(state: ...). Use this when your client app or another system already owns persistence, when you need stateless server deployments, or when you want to encrypt conversation data outside Genkit.

In both modes, the high-level AgentChat tracks continuity for you. Under the hood, server-managed chats carry forward a sessionId/snapshotId, while client-managed chats carry forward the full SessionState.

The SessionState contains three main fields:

  • custom is your application-specific data. Use it for lightweight status variables, preferences, selected options, or list state (e.g. { 'tasks': [] }) that tools or the model needs to inspect across turns. Keep this payload small as it is serialized in every snapshot and client payload.
  • messages is the accumulated message history (List<Message>). The runtime appends user prompts and model completions automatically.
  • artifacts is a list of structured, versioned outputs generated by the agent or tools. Use artifacts for larger outputs that the user might download or inspect independently (e.g. files, reports, itineraries).
  • Use custom state for lightweight control parameters and variables that direct the agent’s next model call or UI rendering. Keep it small.
  • Use artifacts for files, patches, or comprehensive reports that the agent produces. Do not store large documents directly in custom state.

Tools and custom agents can update custom state using the functional session.updateCustom() API. Custom state is fully typed: ai.currentSession<State>() and the updateCustom callback receive a typed State? value, where State comes from the agent’s stateSchema. Genkit automatically diffs the result and streams RFC 6902 JSON Patches to the client mid-stream.

@Schema()
abstract class $TaskItem {
int get id;
String get title;
bool get done;
}
@Schema()
abstract class $TaskState {
List<$TaskItem> get tasks;
int get nextId;
}
final session = ai.currentSession<TaskState>()!;
session.updateCustom((state) {
final nextId = state?.nextId ?? 1;
return TaskState(
tasks: [
...?state?.tasks,
TaskItem(id: nextId, title: 'Buy milk', done: false),
],
nextId: nextId + 1,
);
});

Keep custom state serializable, and provide the matching stateSchema when defining your agent (e.g. stateSchema: TaskState.$schema) so the typed state is validated at load time. When state is a loose JSON map rather than a typed class, use a map schema such as SchemanticType.map(SchemanticType.string(), SchemanticType.dynamicSchema()); the updater then receives a typed Map<String, dynamic>?.

Add a store when defining your agent to enable server-managed persistence:

import 'package:genkit/genkit.dart';
import 'package:genkit/io.dart';
final store = FileSessionStore('.sessions');
final weatherAgent = ai.defineAgent(
name: 'weatherAgent',
system: 'You are a helpful weather assistant.',
store: store,
);

On every successful turn, the store saves a completed snapshot capturing the conversation’s exact state. If a turn fails, the state is rolled back to the last successful turn to prevent partial corruption.

For store choices, see Session stores.

Read a snapshot directly by ID or fetch the latest snapshot in a session using the agent’s snapshot methods:

// Read a specific snapshot point:
final snapshot = await weatherAgent.getSnapshot(snapshotId: 'snapshot-123');
// Read the latest point in a conversation:
final latest = await weatherAgent.getSnapshot(sessionId: 'session-123');
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 UI progress indicators and background tracking.

To continue a server-managed conversation from the latest leaf, pass the sessionId:

final chat = weatherAgent.chat(sessionId: 'user-session-123');
await chat.send(text: 'What is the weather in Tokyo?');

To branch or fork a conversation from a specific historical point, pass the snapshotId:

final branch = weatherAgent.chat(snapshotId: 'snapshot-abc-456');
await branch.send(text: 'Assume the user changed their mind.');

If your agent does not use a server store, pass the full state blob back on each subsequent turn:

final chat = weatherAgent.chat(
state: SessionState(
custom: {'tasks': []},
messages: [],
artifacts: [],
),
);
final res = await chat.send(text: 'Add a task.');
// Store the full session state on the client (e.g., local storage or a
// database). `res.state` is only the typed custom state; `res.raw.state` is the
// complete SessionState with messages, custom state, and artifacts.
saveStateOnClient(res.raw.state);

When custom state changes during a turn, the runtime streams incremental RFC 6902 JSON Patch chunks. AgentChat applies them automatically, yielding the updated state on chunk.custom and chat.state.

final turn = taskAgent.chat().sendStream(text: 'Add buy milk to my list.');
await for (final chunk in turn.stream) {
if (chunk.custom != null) {
updateTodoListUi(chunk.custom!);
}
}

The first patch emitted in a turn is a whole-document replace that aligns the client’s state baseline with the server’s.

Record independent artifacts (such as plans, diffs, or images) from tools or custom agents using the active session:

final session = ai.currentSession()!;
session.addArtifacts([
Artifact(
name: 'report.md',
parts: [TextPart(text: '# Research Report\nThis is the content.')],
metadata: {'contentType': 'text/markdown'},
),
]);

Artifacts with identical names overwrite earlier ones, while unnamed artifacts are appended to the session.