Skip to content

Genkit Go v1.13.0

Choose a tag to compare

@apascal07 apascal07 released this 03 Sep 16:18
de319ca

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.0

Generate 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, and OPENAI_PROJECT_ID for every client it builds, so requests to DeepSeek or xAI carried OpenAI-Organization and OpenAI-Project, and the Anthropic compat plugin sent the OpenAI key as its bearer token when ANTHROPIC_API_KEY was unset. Init now clears all three; the openai plugin 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 WithCacheTTL and WithCacheName cancelled 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}/check and {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 .prompt files sharing a middleware leaked into each other, and concurrent dispatch raced. Prototypes now register by value; *Retry still satisfies ai.Middleware.
  • Every built-in middleware config field carries a description the Dev UI shows as a tooltip, and the Statuses fields of Retry and Fallback offer 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 as If true. Descriptions moved to jsonschema_description, and a schema test rejects description= inside a jsonschema tag.
  • Bidi connections prefer completion over cancellation. Send no longer reports CANCELLED for 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 New failure 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, Content and Metadata included.
  • Each tool-loop turn's generate span records the messages that turn sent, built after the WrapGenerate hooks. The duplicate turn-zero generate span is gone, each turn gets its own *ModelRequest, a resume keeps WithStepName, and util actions keep t:action in 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.
  • googlegenai defaults to a plain HTTP client, like every other plugin and runtime, which removes the extra HTTP spans from the Dev UI. Set HTTPClient with an otelhttp transport to opt back in.
  • The agent conformance spec gains a requires capability gate. Go declares resumable-failures and resumable-aborts and 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 for genkit.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. GenerateText returns the partial's text beside a post-processing error where it returned "".
  • The typed helpers return ai.ErrGenerationBlocked for a blocked response where they returned a schema error, and a string Out for a response with nothing to extract leaves the text on resp.Text().
  • In ai/exp: resume accepts failed and aborted snapshots; 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 writes aborted where it wrote failed; the abort companion answers aborting for a live detached row; and a custom agent returning a non-nil TurnResult beside an error now commits that turn.
  • Hand-built /check-operation/{model} keys no longer resolve. Operation.Action still carries the start key.
  • googlegenai no longer installs an OpenTelemetry HTTP transport by default.
  • A generate span 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.