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.
State strategies
Section titled “State strategies”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.
Understand session state
Section titled “Understand session state”The full state object has three user-visible pieces:
type SessionState<S> = { custom?: S; messages?: MessageData[]; artifacts?: Artifact[];};customis 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.messagesis 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.artifactsis 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 vs. artifacts
Section titled “Custom state vs. artifacts”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.
Modify custom state
Section titled “Modify custom state”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.
Server-managed stores
Section titled “Server-managed stores”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.
Snapshots
Section titled “Snapshots”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:
| Status | Meaning |
|---|---|
pending | A detached background invocation is still running. |
completed | The snapshot captures a settled, resumable state. |
failed | The invocation failed. Error details are stored on the snapshot. |
aborted | The detached invocation was canceled. |
expired | A 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.
Resume by session or snapshot
Section titled “Resume by session or snapshot”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.
Client-managed state
Section titled “Client-managed state”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.
Live custom state
Section titled “Live custom state”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
Section titled “Artifacts”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.
Client transforms
Section titled “Client transforms”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.
State strategies
Section titled “State strategies”Genkit agents keep continuity in one of two ownership models.
Server-managed state uses aix.WithSessionStore(store). The server persists snapshots and callers continue with aix.WithSessionID or aix.WithSnapshotID. Choose this for durable conversations, background execution, snapshot reads, branching, or clients that should not hold conversation history.
Client-managed state omits a store. The caller receives AgentOutput.State and sends it back with aix.WithState. Choose this when your service already stores session data, when you need stateless Genkit workers, or when another system controls encryption and retention.
Server-managed state is the default recommendation for user-facing conversational apps. Client-managed state is useful when you need tighter control over where state is stored and how it moves between services.
go/samples/basic-agents-server serves both models side by side: a chat agent with a store, which answers with a session ID to send back, and a statelessChat agent without one, which answers with the whole state for the client to hold.
Understand session state
Section titled “Understand session state”Session state contains messages, typed custom state, artifacts, and the framework-owned session ID:
import ( "github.com/firebase/genkit/go/ai" aix "github.com/firebase/genkit/go/ai/exp")type SessionState[State any] struct { Artifacts []*aix.Artifact `json:"artifacts,omitempty"` Custom State `json:"custom,omitempty"` Messages []*ai.Message `json:"messages,omitempty"` SessionID string `json:"sessionId,omitempty"`}Customis your typed application state. Use it for compact data the agent needs across turns, such as workflow status, selected records, preferences, task state, or progress.Messagesis conversation history.Artifactscontains generated outputs that the app or user may inspect independently.
Custom state vs. artifacts
Section titled “Custom state vs. artifacts”Custom 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, selected records, preferences, task state, or progress. It rides in every snapshot, so keep it small.
- Use artifacts for generated outputs the user may inspect, reuse, or version independently, such as reports, files, patches, or documents.
Do not put large generated documents into Custom just because they serialize to JSON; make them artifacts.
Modify custom state
Section titled “Modify custom state”Tools and custom agents update typed custom state through the active session. In a custom agent, call sess.UpdateCustom:
sess.UpdateCustom(func(state TravelState) TravelState { state.Status = "Checking weather" state.LastCity = city return state})A tool reaches the same session through its context with aix.SessionFromContext[State]. The updater takes and returns the state as its own Go type, so a mistyped field is a compile error rather than a lost key.
SessionFromContext[State] returns nil in two cases: there is no session in context, and the active session’s state type is not State. A tool shared between agents with different state types therefore gets nil from the mismatching agent, with no error. Fail closed on nil. Return a status.ErrFailedPrecondition (or status.ErrPermissionDenied when the state carries scoping) rather than treating nil as an empty identity, or the tool silently drops whatever it derived from state.
import ( aix "github.com/firebase/genkit/go/ai/exp" "github.com/firebase/genkit/go/core/status" genkitx "github.com/firebase/genkit/go/genkit/exp")
// OrderState is the agent's custom state type.type OrderState struct { Drinks []string `json:"drinks,omitempty"`}addToOrder := genkitx.DefineTool(g, "addToOrder", "Records one drink the customer ordered.", func(ctx context.Context, in struct { Drink string `json:"drink"` }) (string, error) { sess := aix.SessionFromContext[OrderState](ctx) if sess == nil { return "", status.Errorf(status.ErrFailedPrecondition, "addToOrder must be called inside a session") } sess.UpdateCustom(func(order OrderState) OrderState { order.Drinks = append(order.Drinks, in.Drink) return order }) return "Added " + in.Drink + " to the order.", nil })What a tool writes is visible to the next turn’s prompt render, so an agent can turn its own state back into instructions. ai.WithSystemFn runs once per turn and its result is used verbatim, which is what lets the wording branch on the state:
barista := genkitx.DefineAgent(g, "barista", aix.InlinePrompt{ ai.WithModelName("googleai/gemini-flash-latest"), ai.WithTools(addToOrder), ai.WithSystemFn(func(ctx context.Context, _ any) (string, error) { sess := aix.SessionFromContext[OrderState](ctx) if sess == nil { return "", status.Errorf(status.ErrFailedPrecondition, "barista prompt rendered outside a session") } order := sess.Custom() if len(order.Drinks) == 0 { return "You are a brisk barista. Take the order one drink at a time.", nil } return fmt.Sprintf("You are a brisk barista. Ordered so far: %s.", strings.Join(order.Drinks, ", ")), nil }), }, aix.WithSessionStore(store),)The barista.go file of go/samples/basic-agents runs this pair end to end. Note the omitempty on the state’s slice field: a nil Go slice marshals to null, which does not satisfy the array schema inferred from the type.
The runtime streams custom patches as state changes. AgentConnection.Receive applies those patches, and AgentConnection.Custom() returns the current custom state observed by the connection.
Server-side stores
Section titled “Server-side stores”WithSessionStore switches the agent to server-managed state. The store must support snapshot reads and writes. Background detach and abort support also require the store to implement SnapshotSubscriber.
import ( "github.com/firebase/genkit/go/ai/exp/localstore" genkitx "github.com/firebase/genkit/go/genkit/exp")store, err := localstore.NewFileSessionStore[TravelState]("./.genkit/snapshots/travel")if err != nil { // Fails if the snapshot directory cannot be created or is not writable. log.Fatalf("open travel store: %v", err)}
agent := genkitx.DefineAgent(g, "travel", aix.InlinePrompt{ ai.WithModelName("googleai/gemini-flash-latest"), ai.WithSystem("You are a travel assistant."), }, aix.WithSessionStore(store),)FileSessionStore persists snapshots as JSON files and is safe for concurrent use. It can also notify status subscribers when snapshots change, which enables abort handling and efficient wait loops.
For store options and custom store implementation guidance, see Session stores.
Snapshot reads
Section titled “Snapshot reads”Use the typed agent methods when reading snapshots locally:
snap, err := agent.GetSnapshot(ctx, snapshotID)latest, err := agent.GetLatestSnapshot(ctx, sessionID)settled, err := agent.WaitForSnapshot(ctx, snapshotID) // blocks until the row settlesmeta, err := agent.GetSnapshot(ctx, snapshotID, aix.WithMetadataOnly()) // status and timestamps, State nilThese methods apply WithStateTransform and read-time shaping, such as reporting a detached run whose heartbeat went stale as expired. Reading agent.Store() directly returns raw, untransformed state. aix.WithMetadataOnly() skips the conversation entirely on a store that implements aix.SnapshotMetadataReader, which the bundled stores and the Firestore store do.
Snapshot lifecycle
Section titled “Snapshot lifecycle”| Status | Meaning |
|---|---|
pending | A detached invocation is still running. |
aborting | An abort stopped the work and the worker is saving what it finished. |
completed | The turn finished. The snapshot captures settled state and can be resumed. |
failed | The turn broke. The snapshot holds the tool rounds it completed and the structured error, and can be resumed. |
aborted | The caller stopped the run. The snapshot holds the turns that finished, and can be resumed. |
expired | A pending or aborting snapshot’s heartbeat is stale. This status is computed on read and is not written back to the store. |
A turn that finishes writes completed. A turn that breaks writes failed, and a run its caller stops, by cancelling the context, letting a deadline expire, or aborting a detached run, writes aborted. Both keep the turns that ended at a seam and drop the one that did not, so both are resume points, and the output names them in SnapshotID. See Agent error handling.
Status is the persistence lifecycle, not the outcome. A turn that ends on an interrupt still writes completed, because it is resumable. To detect an interrupt, read snap.FinishReason, which is a separate field:
paused := snap.Status == aix.SnapshotStatusCompleted && snap.FinishReason == aix.AgentFinishReasonInterruptedFinishReason carries the semantic outcome of the turn or invocation the snapshot captured: aix.AgentFinishReasonStop, AgentFinishReasonInterrupted, AgentFinishReasonFailed, AgentFinishReasonAborted, and the other values on aix.AgentFinishReason. See Agent interrupts for what a paused turn allows next.
Invocation options
Section titled “Invocation options”Use WithSessionID when the caller tracks a conversation:
out, err := agent.RunText(ctx, "What about Paris?", aix.WithSessionID[TravelState](previous.SessionID),)Use WithSnapshotID to branch:
rainPlan, err := agent.RunText(ctx, "Assume it rains.", aix.WithSnapshotID[TravelState](base.SnapshotID),)Use WithState for client-managed agents:
out, err := agent.RunText(ctx, "Add buy milk.", aix.WithState(previous.State),)An empty session ID is rejected. WithState cannot be combined with snapshot or session options.
You may also choose the session ID yourself. If aix.WithSessionID(id) resolves no existing snapshot, the runtime starts a fresh conversation under that ID and stamps it on every snapshot it persists, so a client can mint a UUID up front instead of waiting for a server-issued ID. SessionID is framework-owned only in the sense that the framework mints one when you supply none, and that for server-managed agents the snapshot row’s ID is canonical.
Changing your state type
Section titled “Changing your state type”Custom state is persisted as plain JSON and decoded with encoding/json. That gives three rules:
- Adding a field is safe. Old snapshots load it as the Go zero value.
- Removing or renaming a field is safe to load, but drops the old data silently. Nothing rejects the unknown key.
- Changing a field’s JSON type is not safe. The decode fails, the store read returns an error, and the resume fails with it.
Keep changes additive. Keep an old field readable with its original json tag through a migration window, and carry your own version field inside Custom when you need to branch on shape. There is no schema version on the snapshot envelope.
State and stream transforms
Section titled “State and stream transforms”WithStateTransform redacts or reshapes session state on the way out to clients. It applies to snapshot reads and client-managed output, not to persisted raw state or the agent function’s internal view.
agent := genkitx.DefineAgent(g, "support", aix.InlinePrompt{ ai.WithSystem("Summarize support cases."), }, aix.WithSessionStore(store), aix.WithStateTransform(func(ctx context.Context, state *aix.SessionState[SupportState]) (*aix.SessionState[SupportState], error) { state.Custom.InternalNotes = "" return state, nil }),)aix.WithStreamTransform[State](fn) runs on every streamed chunk at the wire boundary. It takes a func(ctx context.Context, chunk *aix.AgentStreamChunk) (*aix.AgentStreamChunk, error). A chunk carries no state type, so State cannot be inferred and must be written out.
aix.WithStreamTransform[SupportState](func(ctx context.Context, chunk *aix.AgentStreamChunk) (*aix.AgentStreamChunk, error) { if chunk.Artifact != nil && chunk.Artifact.Name == "internal-notes" { // Drop the chunk from the wire; the session still records the artifact. return nil, nil } return chunk, nil})AgentStreamChunk has four fields, and more than one can be set on a single chunk:
| Field | Type | Contents |
|---|---|---|
ModelChunk | *ai.ModelResponseChunk | Generation tokens from the model. |
Artifact | *aix.Artifact | A newly produced artifact. |
CustomPatch | aix.JSONPatch | An RFC 6902 delta to custom state. |
TurnEnd | *aix.TurnEnd | The turn-end signal, including the new snapshot ID. |
The chunk is a fresh deep copy the transform owns, so mutating it in place is safe. Returning nil drops the chunk from the wire only: side effects and the final AgentOutput keep the data. Never drop a chunk whose TurnEnd is set, because clients pace the conversation on that signal; reshape it instead. Returning an error fails the whole invocation, which is the fail-closed behavior you want when a chunk cannot be shaped safely.
Prefer WithStateTransform for custom state redaction. The runtime applies it before diffing, so the patch stream stays consistent; rewriting CustomPatch in a stream transform desyncs clients that rebuild custom state from the patch sequence.
Custom state and artifacts
Section titled “Custom state and artifacts”Prompt-backed and custom agents can update state through the active session. Custom patches are streamed automatically when custom state changes. AgentConnection.Receive applies those patches and AgentConnection.Custom() returns the live custom state observed so far.
An artifact is a named collection of parts:
type Artifact struct { Metadata map[string]any `json:"metadata,omitempty"` Name string `json:"name,omitempty"` Parts []*ai.Part `json:"parts"`}Responder.SendArtifact takes a pointer. It both streams the artifact and records it in the session, so the artifact is available in the final output or snapshot:
resp.SendArtifact(&aix.Artifact{ Name: "meal-plan", Parts: []*ai.Part{ai.NewTextPart(draft)}, Metadata: map[string]any{"contentType": "text/markdown"},})Read them back from out.Artifacts on an *aix.AgentOutput[State], from snap.State.Artifacts on a snapshot, or from sess.Artifacts() inside a run. SendArtifact appends and does not deduplicate names, so two sends with the same Name produce two entries. Call sess.UpdateArtifacts when you want to replace one instead.
Next steps
Section titled “Next steps”- Session stores covers the built-in stores and the
SessionStoreinterface a custom store implements. - Background execution covers detached turns and pending snapshots.
State strategies
Section titled “State strategies”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.
Understand session state
Section titled “Understand session state”The SessionState contains three main fields:
customis 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.messagesis the accumulated message history (List<Message>). The runtime appends user prompts and model completions automatically.artifactsis 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).
Custom state vs. artifacts
Section titled “Custom state vs. artifacts”- 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.
Modify custom state
Section titled “Modify 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>?.
Server-managed stores
Section titled “Server-managed stores”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.
Snapshots
Section titled “Snapshots”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');Snapshot lifecycle
Section titled “Snapshot lifecycle”| Status | Meaning |
|---|---|
pending | A detached background invocation is still running. |
completed | The snapshot captures a settled, resumable state. |
failed | The invocation failed. Error details are stored on the snapshot. |
aborted | The detached invocation was canceled. |
expired | A 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.
Resume by session or snapshot
Section titled “Resume by session or snapshot”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.');Client-managed state
Section titled “Client-managed state”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);Live custom state
Section titled “Live custom 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.
Artifacts
Section titled “Artifacts”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.
State strategies
Section titled “State strategies”Genkit agents can keep conversation continuity in one of two ways:
Server-managed state means the agent has a store (for example FileSessionStore or InMemorySessionStore). The server persists messages, custom state, and artifacts between turns. Clients continue the chat by sending a session_id (loads latest state) or snapshot_id (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 session at the end of each turn, and the client must echo that state back on the next turn using chat(messages=..., state=..., artifacts=...). 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, AgentChat tracks continuity for you. Server-managed chats carry forward a session_id / snapshot_id. Client-managed chats carry forward messages, custom state, and artifacts.
Understand session state
Section titled “Understand session state”Session state has three main fields:
customis your application-specific data. Use it for lightweight status variables, preferences, selected options, or list state that tools or the model needs across turns. Keep this payload small as it is serialized in every snapshot and client payload.messagesis the accumulated message history. The runtime appends user prompts and model completions automatically.artifactsis 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.
Custom state vs. artifacts
Section titled “Custom state vs. artifacts”- 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.
Modify custom state
Section titled “Modify custom state”Tools and custom agents can update custom state using session.update_custom(). With a state_schema, chat.state, response.state, and streamed chunk.custom come back as that Pydantic model. Genkit automatically diffs the result and streams RFC 6902 JSON Patches to the client mid-stream.
from pydantic import BaseModel
class TaskItem(BaseModel): id: int title: str done: bool = False
class TaskState(BaseModel): tasks: list[TaskItem] = [] next_id: int = 1
sess = ai.current_session()assert sess is not None
def mutate(custom: TaskState | None) -> TaskState: state = custom or TaskState() next_id = state.next_id new_task = TaskItem(id=next_id, title='Buy milk') return TaskState(tasks=[*state.tasks, new_task], next_id=next_id + 1)
await sess.update_custom(mutate)Keep custom state serializable, and provide the matching state_schema when defining your agent so the typed state is validated at load time.
Server-managed stores
Section titled “Server-managed stores”Add a store when defining your agent to enable server-managed persistence:
from genkit.agent import FileSessionStore
store = FileSessionStore('.sessions')
weather_agent = ai.define_agent( name='weatherAgent', model='googleai/gemini-flash-latest', 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 resume handle stays on the last successful snapshot so the next turn does not continue from a broken partial state.
For store choices, see Session stores.
Snapshots
Section titled “Snapshots”Read a snapshot directly by ID or fetch the latest snapshot in a session:
snapshot = await weather_agent.get_snapshot(snapshot_id='snapshot-123')latest = await weather_agent.get_snapshot(session_id='session-123')Snapshot lifecycle
Section titled “Snapshot lifecycle”| Status | Meaning |
|---|---|
pending | A detached background invocation is still running. |
completed | The snapshot captures a settled, resumable state. |
failed | The invocation failed. Error details are stored on the snapshot. |
aborted | The detached invocation was canceled. |
expired | A 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.
Resume by session or snapshot
Section titled “Resume by session or snapshot”To continue a server-managed conversation from the latest leaf, pass the session_id:
chat = weather_agent.chat(session_id='user-session-123')await chat.send('What is the weather in Tokyo?')To branch from a specific historical point, pass the snapshot_id (or use load_chat(snapshot_id=...)):
branch = await weather_agent.load_chat(snapshot_id='snapshot-abc-456')await branch.send('Assume the user changed their mind.')Client-managed state
Section titled “Client-managed state”If your agent does not use a server store, capture messages, custom state, and artifacts yourself, then pass them back:
chat = weather_agent.chat()res = await chat.send('My name is Ada. Remember it.')
messages, state, artifacts = chat.messages, chat.state, chat.artifacts
resumed = weather_agent.chat(messages=messages, state=state, artifacts=artifacts)await resumed.send('What is my name?')Live custom state
Section titled “Live custom 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.
turn = task_agent.chat().send_stream('Add buy milk to my list.')
async for chunk in turn.stream: if chunk.custom is not None: update_todo_list_ui(chunk.custom)Artifacts
Section titled “Artifacts”Record independent artifacts from tools or custom agents using the active session:
from genkit import Part, TextPartfrom genkit.agent import Artifact
sess = ai.current_session()assert sess is not None
await sess.add_artifacts([ Artifact( name='report.md', parts=[Part(root=TextPart(text='# Research Report\nThis is the content.'))], )])Artifacts with identical names overwrite earlier ones, while unnamed artifacts are appended to the session.
Client transforms
Section titled “Client transforms”Use state_transform and chunk_transform to redact or reshape what leaves the server before it reaches a client:
from genkit.agent import SessionState
def redact_state(state: SessionState) -> SessionState: # Drop secrets before the client sees them. return state
agent = ai.define_agent( name='supportAgent', model='googleai/gemini-flash-latest', system='Help customers.', state_transform=redact_state,)