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/experimental.dart';
import 'package:genkit/experimental_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 or is aborted, the store saves a failed or aborted snapshot carrying the error details (on failure) and the last-good state from before that turn so it can be rerun without 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. Pass metadataOnly: true to read status and metadata without loading the conversation state:

// Read a specific snapshot point:
final snapshot = await weatherAgent.getSnapshot(snapshotId: 'snapshot-123');
// Read metadata only for the latest point in a conversation:
final latest = await weatherAgent.getSnapshot(
sessionId: 'session-123',
metadataOnly: true,
);
StatusMeaning
pendingA detached background invocation is still running.
abortingAn abort has been requested; the background worker is unwinding and saving its progress.
completedThe snapshot captures a settled, resumable state.
failedThe turn failed. Error details and the last-good history before the turn are stored.
abortedThe turn was cancelled or hit maxTurns. The last-good history before the turn is stored.
expiredA pending snapshot heartbeat went stale, so the background worker is presumed dead.

completed, failed, and aborted snapshots are valid resume points (expired is terminal and not resumable). 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.