Warning
Retracted. This version ships the A2UI preview at github.com/firebase/genkit/go/plugins/a2ui, not at the plugins/a2ui/exp path the notes below describe. Use v1.13.1: it moves the package to the documented path and retracts this version in go.mod. Everything else below applies to v1.13.1 unchanged.
Progress survives failure. A generate call that fails or is stopped returns the conversation up to its last completed tool round, beside the classified error. An agent commits that conversation as a failed or aborted snapshot, and both resume. Sub-agents run in the background, get waited on or aborted, and pick up where they left off from any process holding the task ID. Beyond that, the experimental A2UI plugin lets an agent stream interactive UI to a browser.
go get github.com/firebase/genkit/go@v1.13.0Generate returns what it finished, even on failure
Once the request has resolved, Generate returns the partial response beside its error:
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Plan the trip."),
ai.WithTools(searchFlights, bookHotel),
ai.WithMaxTurns(3),
)
if err != nil && resp != nil {
resp.History() // The completed rounds. Send them back to retry the failed step.
resp.FinishReason // Failed if something broke, Aborted if the caller stopped it.
resp.FinishMessage // The cause, e.g. "exceeded maximum tool call iterations (3)".
resp.Error // The same cause, classified: Status, Message, Details.
}ai.FinishReasonFailed means something broke: a model call or a tool. ai.FinishReasonAborted means the caller stopped it: a cancelled context, an expired deadline, or a limit such as WithMaxTurns. resp.Error is the classified form of FinishMessage, so a response read back from a trace or a persisted turn still says why it stopped.
History() ends at a turn seam: the completed rounds of model message plus every tool response, and nothing from the turn that failed. No provider accepts a conversation ending in an unanswered tool request, so a failed tool drops its whole round, including the model message that opened it. Send the history back to retry the failed step without repeating the tool calls that succeeded. Text streamed before the failure already reached your callback.
GenerateStream and GenerateDataStream yield the same partial beside their error, Done and carrying Response.
Values survive the action boundary
Action.Run zeroed its output on any error, the JSON surface marshaled nothing, and the trace recorded output only on success. All three now carry whatever the function returned: a flow that returns a value beside an error hands it to its caller, an output that failed schema validation comes back with its error, and a failed generate's conversation shows up in the Dev UI trace.
A blocked response is an error, not a schema mismatch
GenerateData, GenerateDataStream, DataPrompt.Execute, and DataPrompt.ExecuteStream parsed a safety-blocked response and reported Expected: object, given: null. They now return ai.ErrGenerationBlocked, a FAILED_PRECONDITION subtype, with the response alongside:
out, resp, err := genkit.GenerateData[Itinerary](ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Plan a week in Kyoto."),
)
if errors.Is(err, ai.ErrGenerationBlocked) {
log.Printf("refused: %s", resp.FinishMessage)
}Interrupts, tool requests, and empty responses keep a nil output and a nil error. Streamed chunks parse before any finish reason exists, so the terminal value settles the call. Generate still hands a blocked response back as a value.
Resume any turn, whether it succeeded, failed, or was aborted
Agents in ai/exp build on the partial. A failed turn commits the tool rounds it completed as a failed snapshot carrying the error, and resume accepts it:
out, _ := agent.RunText(ctx, "Book the full itinerary.")
if out.FinishReason == aix.AgentFinishReasonFailed {
snap, _ := agent.GetSnapshot(ctx, out.SnapshotID)
snap.Status // aix.SnapshotStatusFailed, no longer a dead end
snap.Error // the classified failure, same as out.Error
snap.State.Messages // the tool rounds the turn completed
}Re-attempt with an input that has no payload. The turn runs again on the committed messages, so the tool calls that succeeded are not repeated:
retried, err := agent.Run(ctx, &aix.AgentInput{}, aix.WithSessionID[any](out.SessionID))A new message works on that snapshot too, and rewinding past the failure is a resume from the previous snapshot ID. Whether to retry is your call: the runtime records the status and never judges it.
Every failure past the model call commits. A turn rejected before it reached the model rolls back, and the resume point stays the turn before. AgentOutput.SnapshotID names the latest resumable state either way. Custom agents opt in by returning a TurnResult beside the error; a bare error still discards the turn. Without a store, the failed output's State carries the same resume point inline.
The turn-end snapshot now writes on a context that outlives the turn's own, so a turn cancelled from outside still lands its snapshot.
aborted means the caller stopped it
aborted now covers every way a caller ends a run, and failed every way one breaks. A cancelled context, a closed transport, an expired deadline, a limit such as ai.WithMaxTurns, or Abort on a detached run: each lands an aborted snapshot holding the turns that finished, and each resumes like a failed one. Run returns that snapshot's output beside the error instead of nil:
ctx, cancel := context.WithCancel(ctx)
out, err := chatAgent.Run(ctx, &aix.AgentInput{Message: msg}) // cancel() elsewhere
// err is what stopped the run; out names where it stopped.
if out.FinishReason == aix.AgentFinishReasonAborted {
resumed, _ := chatAgent.Run(context.Background(), &aix.AgentInput{},
aix.WithSnapshotID[any](out.SnapshotID))
}The turn in flight is discarded whole. A tool that ran inside it runs again on resume.
Wind-down has its own status: aborting
A detached run reaches aborted in two writes: the flip that stops the work, and the finalize that stamps the state on. The row between them was aborted with no state, shaped as pending. It is now aborting, a shared wire status. The worker keeps heartbeating through its wind-down for up to five minutes, so a wedged drain reads as expired instead of hanging forever. WaitForSnapshot waits through the window, and the abort companion answers aborting where it answered aborted.
The basic-agents CLI shows all of it: a broken or stopped turn is offered like any other, and an empty line re-runs the turn it left unanswered.
Sub-agents run in the background and pick up where they left off
Reach any agent by name with AgentHandle
AgentHandle is the caller-side view of an agent for code that knows it only by name (orchestrators, middleware, tools), with custom state as json.RawMessage. One lookup replaces the action lookup, the BidiAction assertion, and the JSON marshaling:
h := genkitx.LookupAgent(g, "researcher") // nil on a miss; or agent.Handle()
out, err := h.RunText(ctx, task,
aix.WithState(&aix.SessionState[json.RawMessage]{Messages: history}))RunDetached is the one-shot counterpart of AgentConnection.Detach. A DetachedTask is a snapshot ID plus the agent that minted it, so any process can rehydrate it:
task, err := agent.RunDetached(ctx, &aix.AgentInput{Message: msg})
id := task.SnapshotID() // record it
task = agent.Task(id) // any process, any time later
snap, err := task.Poll(ctx) // one read
snap, err = task.Wait(ctx) // blocks until it settles
status, err := task.Abort(ctx)POST /agents/{name}/waitForSnapshot is the blocking counterpart of getSnapshot: one request follows a detached run to completion, and a trace carries one span per wait instead of one per tick. Handle calls are shaped like a remote client's: the state transform applies, a stale pending row reads as expired, and errors match by status name, so an HTTP-backed handle is a second implementation rather than a second surface.
GetSnapshot, GetLatestSnapshot, and Poll take aix.WithMetadataOnly(), which returns status, finish reason, parent, and timestamps without the conversation. Stores that implement the optional SnapshotMetadataReader (the bundled local stores and Firestore) skip loading the history; Firestore answers with one document read. Other stores keep compiling and are read in full.
Delegate without waiting
With Async set on the Agents middleware, every delegation tool takes a background flag that returns a task ID at once, and three shared tools control what was launched:
researcher := genkitx.DefineAgent(g, "researcher",
aix.InlinePrompt{
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("You are a thorough research assistant."),
},
aix.WithDescription[any]("Researches a topic and summarizes well-sourced findings."),
// A background delegation is tracked by a snapshot, so the sub-agent needs a store.
aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()),
)
ai.WithUse(&middlewarex.Agents{
Agents: []aix.AgentRef{researcher.Ref()},
Async: true,
})delegate_to_researcher {task, name?, background: true}
-> {response, taskId: "researcher:<snapshotId>", status: "pending"}
check_background_tasks {taskIds}
-> {tasks: [{taskId, agent, status, response?, artifacts?, error?}]}
wait_for_background_tasks {taskIds, timeoutSeconds?, waitFor?: "all" | "first"}
-> the same, plus timedOut
abort_background_tasks {taskIds}
-> the same, each task reported where the stop left it
The middleware keeps no task registry. The task ID rides in the tool result, so the orchestrator's history is the registry, and an orchestrator rebuilt from that history can still collect. wait_for_background_tasks follows tasks through waitForSnapshot, so each is reported the moment it settles; timeoutSeconds turns a slow task into an interim answer, and waitFor: "first" turns the join into a race. Abort never loses an answer: a finished task reports its result, and a live one reports aborting while it saves its progress. The optional name on a delegation is a label echoed beside the taskId.
The basic-agents sample gained an incident commander built on this: two investigators launched in the background, a status update posted while they run, and results collected with a short timeout first.
Continue a delegation instead of restarting it
Every settled server-managed delegation returns a taskId naming its last committed snapshot, and continue_task spends it:
continue_task {taskId, instructions?, background?}
-> a delegation result, or a fresh pending handle when background
A failed or aborted task continues from its last saved progress: empty instructions re-attempt the committed turn, non-empty ones steer the retry. A completed task takes follow-up instructions inside the sub-agent's own session. An expired task is abort-fenced, then continued from its parent snapshot. An interrupted task is refused, since continuing it would mean answering the interrupt. The tool registers only when a configured sub-agent can leave a handle behind.
Stream interactive UI with A2UI
The new preview a2ui plugin adds A2UI support: an agent streams interactive surfaces that a client renders incrementally. The integration is one middleware:
import a2uix "github.com/firebase/genkit/go/plugins/a2ui/exp"
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("You help users. Render UI when it is clearer than prose."),
ai.WithPrompt("show me the weather in Tokyo"),
ai.WithUse(&a2uix.Surfaces{}), // defaults to the bundled 'basic' catalog
)
// A2UI envelopes ride as data parts on the response message.
envelopes := a2uix.EnvelopesFromParts(resp.Message.Content)Surfaces injects the catalog's capabilities into the system prompt, extracts a2ui fenced blocks from streamed chunks and the final message, validates them against the catalog, and rewrites them into data parts with mime type application/a2ui+json. The envelopes are byte-compatible with the JS and Dart plugins and the @a2ui/* renderers. Register your own components with LoadCatalog or LoadCatalogFile and reference them by CatalogID. Validate (warn, strict, off) checks structure and component names, not prop values: treat rendered surfaces as untrusted model output.
Register &a2ui.A2UI{} as a plugin to reference the middleware by name from .prompt files and the Dev UI. go/samples/basic-middleware/a2ui serves the endpoint the browser frontend in js/testapps/a2ui/web expects, so the existing web UI works unchanged against Go.
Smaller things worth knowing
- The OpenAI-compatible family no longer forwards OpenAI's identity. The SDK reads
OPENAI_API_KEY,OPENAI_ORG_ID, andOPENAI_PROJECT_IDfor every client it builds, so requests to DeepSeek or xAI carriedOpenAI-OrganizationandOpenAI-Project, and the Anthropic compat plugin sent the OpenAI key as its bearer token whenANTHROPIC_API_KEYwas unset.Initnow clears all three; theopenaiplugin sets them itself. - Gemini explicit context caching no longer pays twice. The cached prefix was also sent inline, every request created a new cache, the content hash could not tell two caches apart, and
WithCacheTTLandWithCacheNamecancelled each other. The prefix now leaves the wire once cached, a matching cache is reused, and the markers compose:ai.NewUserTextMessage(doc).WithCacheTTL(3600).WithCacheName(known). Gemini 2.5 and newer cache repeated prefixes implicitly at no storage charge, so the explicit path earns its cost only when the hit must be guaranteed. - Background-action companions register as
{name}/checkand{name}/cancel, the form JS, Python, and the Dev UI use, so the Dev UI's background-task panel works against Go background models. - Each JSON-dispatched middleware call gets its own config. Pointer prototypes were decoded into in place, so one call's field leaked into the next, two
.promptfiles sharing a middleware leaked into each other, and concurrent dispatch raced. Prototypes now register by value;*Retrystill satisfiesai.Middleware. - Every built-in middleware config field carries a description the Dev UI shows as a tooltip, and the
Statusesfields ofRetryandFallbackoffer the status names as an enum.status.Names()returns them in gRPC code order. - Tool descriptions no longer truncate at the first comma:
description=If true, descend into subdirectories.reached the model asIf true. Descriptions moved tojsonschema_description, and a schema test rejectsdescription=inside ajsonschematag. - Bidi connections prefer completion over cancellation.
Sendno longer reportsCANCELLEDfor a teardown that was the action finishing, and a committed result is no longer replaced by the caller's deadline, which is what keeps a detached agent handoff from reading as a failed launch. - A middleware
Newfailure keeps its classification instead of collapsing to INVALID_ARGUMENT. - Resumed tool messages are ordered by their requests' positions, as first-run ones already were, and an interrupt replay restores a resolved sibling's full multipart response,
ContentandMetadataincluded. - Each tool-loop turn's
generatespan records the messages that turn sent, built after theWrapGeneratehooks. The duplicate turn-zerogeneratespan is gone, each turn gets its own*ModelRequest, a resume keepsWithStepName, and util actions keept:actionin their trace paths. - Reasoning parts survive the wire. A signature-less part no longer carries an empty metadata map, so the Dev UI shows one Reasoning box per thought, and an empty reasoning part no longer round-trips as text.
googlegenaidefaults to a plain HTTP client, like every other plugin and runtime, which removes the extra HTTP spans from the Dev UI. SetHTTPClientwith anotelhttptransport to opt back in.- The agent conformance spec gains a
requirescapability gate. Go declaresresumable-failuresandresumable-abortsand runs every case; JS and Python skip those eight until they adopt the behavior. - The Go README covers re-attempting failed turns, stopping and continuing a run, background delegation, and
continue_task. The godoc examples forgenkit.Handler,genkit.HandlerFunc,DefineStreamingFlow, and the telemetry plugins compile when pasted.
Before you upgrade
No signatures change. Each of these is a value or a status that now arrives where it used to be absent.
- Any action, flow, or tool that returns a value beside an error now hands that value to its caller. Code that branches on the value without checking the error will notice.
GenerateTextreturns the partial's text beside a post-processing error where it returned"". - The typed helpers return
ai.ErrGenerationBlockedfor a blocked response where they returned a schema error, and a stringOutfor a response with nothing to extract leaves the text onresp.Text(). - In
ai/exp: resume acceptsfailedandabortedsnapshots;AgentInput{}runs a turn on a session that has messages; a turn ended by a cancelled context, an expired deadline, or a caller-set limit writesabortedwhere it wrotefailed; theabortcompanion answersabortingfor a live detached row; and a custom agent returning a non-nilTurnResultbeside an error now commits that turn. - Hand-built
/check-operation/{model}keys no longer resolve.Operation.Actionstill carries the start key. googlegenaino longer installs an OpenTelemetry HTTP transport by default.- A
generatespan carries the same messages as the model span inside it, so tool-loop trace payload roughly doubles; dropping the duplicate turn-zero span offsets part of that.