github genkit-ai/genkit go/v1.14.0
Genkit Go v1.14.0

3 hours ago

Tools and interrupts are stable and typed end to end. A tool that pauses is resumed with the type it declared, a tool error can go back to the model to fix, and an approval can be decided by a judge model or refused with a reason. Every run reports what it spent, a budget can stop or pause it, and context compression keeps long tool loops inside the window. Agents can delegate to agents that other Genkit apps serve, sessions persist in PostgreSQL, and telemetry plugs in with or without OpenTelemetry. A preview plugin adds decision models.

go get github.com/firebase/genkit/go@v1.14.0

Tools that pause are resumed by type

genkit.DefineResumableTool declares a third type: what the tool is resumed with. The tool gets it as a pointer, nil on the first call. The caller claims each paused call with the tool's Interrupted method, which matches the part to the tool and decodes its input, so nothing after the claim can fail.

Before: the caller matches tools by name, reads the input as an untyped map, and the tool reads its answer back one key at a time.

transferMoney := genkit.DefineTool(g, "transferMoney", "...",
	func(tc *ai.ToolContext, in TransferInput) (*TransferOutput, error) {
		if !tc.IsResumed() && in.Amount > approvalLimit {
			return nil, ai.InterruptWith(tc, map[string]any{"reason": "over_limit"})
		}
		if approved, ok := ai.ResumedValue[bool](tc, "approved"); ok && !approved {
			return &TransferOutput{Status: "declined"}, nil
		}
		// ...
	})

var restarts []*ai.Part
for _, part := range resp.Interrupts() {
	switch part.ToolRequest.Name {
	case transferMoney.Name():
		in := part.ToolRequest.Input.(map[string]any) // untyped on the part
		approved := askHuman(in["amount"].(float64), in["toAccount"].(string))
		restart, err := transferMoney.RestartWith(part,
			ai.WithResumedMetadata[TransferInput](map[string]any{"approved": approved}))
		if err != nil {
			return nil, err
		}
		restarts = append(restarts, restart)
	}
}
resp, err = genkit.Generate(ctx, g,
	ai.WithMessages(resp.History()...), ai.WithTools(transferMoney), ai.WithToolRestarts(restarts...))

After: the claim does the match and the type check, and the answer has one type at both ends.

transferMoney := genkit.DefineResumableTool(g, "transferMoney", "Transfers money to another account.",
	func(ctx context.Context, in TransferInput, resume *Confirmation) (*TransferOutput, error) {
		if resume == nil && in.Amount > approvalLimit {
			return nil, tool.Interrupt(ctx, map[string]any{"reason": "over_limit"})
		}
		if resume != nil && !resume.Approved {
			return &TransferOutput{Status: "declined"}, nil
		}
		// ...
	})

var parts []*ai.Part
for _, part := range resp.Interrupts() {
	if call, ok := transferMoney.Interrupted(part); ok {
		approved := askHuman(call.Input.Amount, call.Input.ToAccount) // TransferInput, decoded
		parts = append(parts, call.Restart(Confirmation{Approved: approved}))
	}
}
resp, err = genkit.Generate(ctx, g,
	ai.WithMessages(resp.History()...), ai.WithTools(transferMoney), ai.WithResume(parts...))

call.Respond(out) answers without running the tool again, and call.RestartWithInput(in, res) runs it with revised arguments, which history then records as the request. The tool reads the input it was first called with through tool.OriginalInput[In](ctx). Code that holds only the part, such as a generic handler, builds the same parts untyped with part.ToToolRestart, part.ToToolRestartWithInput, and part.ToToolResponse: the payload is checked against the resumeSchema the tool advertises and decoded into the resume type, so a map with the right fields behaves like the typed value, in process or after a wire hop.

The resume is checked as a whole before any tool runs. A restart that would charge a card no longer runs while a sibling request fails the resume, and WithResume rejects a part that is still paused, such as resp.Interrupts() passed back as is. A resume that pauses again, because the restarted tool asks a second question, returns (resp, nil) with FinishReason interrupted, like any other pause.

The runtime verbs live in ai/tool

The verbs a tool or a WrapTool hook calls are in the stable github.com/firebase/genkit/go/ai/tool package. They take a context.Context, so they work in a DefineTool tool as well, since *ai.ToolContext embeds it:

genkit.DefineTool(g, "deployService", "Deploys a service and reports the rollout.",
	func(ctx *ai.ToolContext, in Deploy) (*Rollout, error) {
		tool.SendChunk(ctx, &ai.ModelResponseChunk{
			Role:    ai.RoleTool,
			Content: []*ai.Part{ai.NewTextPart("rolling out " + in.Service)},
		})
		rollout, chartURL, err := rollOut(ctx, in)
		if err != nil {
			return nil, err
		}
		tool.AttachParts(ctx, ai.NewMediaPart("image/png", chartURL)) // a data: URL
		return rollout, nil // still *Rollout, so the tool keeps its output schema
	})

AttachParts replaces multipart tools: the tool keeps its own output type, so the model sees its real output schema rather than the multipart envelope. SendChunk streams progress from any tool, a restarted one included. Each verb is scoped to the call that runs it, so a Generate inside a restarted tool does not inherit that tool's approval.

Tool errors the model can fix

A tool error still stops the loop with ai.ErrToolFailed by default. A tool author can now hand one error back to the model instead, with tool.Fail, and an app can do the same for whole tools it does not own with middleware.SoftToolErrors:

query := genkit.DefineTool(g, "query", "Runs a read-only SQL query.",
	func(ctx *ai.ToolContext, in QueryInput) ([]string, error) {
		rows, err := runQuery(ctx, db, in.SQL)
		if errors.Is(err, errSyntax) {
			return nil, tool.Fail(ctx, err) // the model reads the error and fixes the query
		}
		return rows, err // a lost connection still stops the loop
	})

resp, err := genkit.Generate(ctx, g,
	ai.WithPrompt("Which city has the most open tickets?"),
	ai.WithTools(mcpTools...),
	ai.WithUse(&middleware.SoftToolErrors{}), // or SoftToolErrors{Tools: []string{"search"}}
)

The model receives {"error": "..."} as the tool's output with isError in the part's metadata, the key MCP uses. Part.IsToolError reads it, and the Anthropic plugin maps it to is_error. SoftToolErrors converts after the whole WrapTool chain, so a retry hook still sees the error first, and interrupts and cancellation never convert. The Filesystem middleware and the MCP plugin's isError results use the same path.

A tool that panics no longer takes the process down. The panic becomes the tool's own error with the fixed message tool panicked, and the value and stack go only to the error log.

Approvals that a model can decide, and a caller can refuse

ToolApproval holds every call that is not on its allowlist. Holds are claimed the way tool interrupts are, with a typed decision, and a refusal reaches the model as the tool's error with your reason:

for _, part := range resp.Interrupts() {
	if call, ok := middleware.ToolApprovalInterrupted(part); ok {
		decision := middleware.ToolCallDecision{Approved: confirm(part.ToolRequest.Name, call.Input)}
		if !decision.Approved {
			decision.Reason = "the user declined"
		}
		parts = append(parts, call.Restart(decision))
	}
	if call, ok := transferMoney.Interrupted(part); ok {
		parts = append(parts, call.Restart(Confirmation{Approved: true}))
	}
}

Silence never approves: any restart of a hold without Approved: true refuses the call, a bare one included. After an approval, the tool runs as a fresh call and can ask its own question, and the restart that answers it passes the gate without a second approval unless it revises the input.

A judge model can decide the calls the allowlist does not cover, the auto-approval mode coding agents offer:

ai.WithUse(&middleware.ToolApproval{
	AllowedTools: []string{"list_files"},
	Judge:        ai.NewModelRef("googleai/gemini-flash-lite-latest", nil),
	JudgePolicy:  "Never allow deleting anything outside build/.",
})

The judge answers allow, deny (the model gets a tool error and continues), or ask (the call is held for you). It fails closed: an error or an answer outside the three holds the call, marked so a resume UI can tell the two apart. The judge sees what the user wrote and the pending call, never model text or tool results, since injected instructions arrive through those.

Middleware authors get the same claim for their own holds. A WrapTool hook pauses with tool.Interrupt, reads its answer with tool.ResumeData[T], skips calls it already let through with tool.Released, and exports a typed claim built on ai.MiddlewareInterrupted[Res](name, part).

Every run reports what it spent

Model plugins now report ai.GenerationUsage under one convention: InputTokens is the whole prompt, with CachedContentTokens and the new CacheWriteTokens as parts of it; OutputTokens excludes ThoughtsTokens; TotalTokens is the provider's total or their sum. Generate sums every model call in a run into TotalUsage, and agents report usage per turn, per invocation, and per session:

resp, _ := genkit.Generate(ctx, g,
	ai.WithModelName("googleai/gemini-flash-latest"),
	ai.WithPrompt("What's the weather in Paris and Tokyo?"),
	ai.WithTools(weather),
)
fmt.Println(resp.Usage.InputTokens)      // the last model call: how full the context is
fmt.Println(resp.TotalUsage.TotalTokens) // every model call in the run, nested ones included
for chunk, err := range conn.Receive() {
	if err != nil {
		break
	}
	if chunk.TurnEnd != nil {
		fmt.Println(chunk.TurnEnd.Usage) // this turn
	}
}
out, _ := conn.Output()
fmt.Println(out.Usage)       // this invocation, failed turns included
fmt.Println(out.State.Usage) // the session so far; snapshots persist it

TotalUsage counts the tool loop, the calls its tools and hooks make (a fallback model, a nested generate), and it is set on a partial response too, since those tokens were billed. Session usage is a ledger: it never rolls back, so a failed turn's spend stays counted. A sub-agent's usage stays in its own session. ai.SumUsage adds usages field by field for code that keeps its own totals.

Cap it with Budget

The experimental Budget middleware stops a run, or an agent session, at a limit. Every non-zero field of Limit is a cap, Custom key by key, so a provider-reported cost works too:

resp, err := genkit.Generate(ctx, g,
	ai.WithModelName("googleai/gemini-flash-latest"),
	ai.WithPrompt("Research the topic and summarize it."),
	ai.WithTools(search),
	ai.WithUse(&middlewarex.Budget{Limit: ai.GenerationUsage{
		TotalTokens: 200_000,
		Custom:      map[string]float64{"cost": 0.50},
	}}),
)
if errors.Is(err, ai.ErrBudgetExceeded) {
	log.Println(resp.FinishReason) // aborted: the caller's limit, not a failure
	_ = resp.History()             // the rounds that ran
}

The limit is checked before each model call, outside every model hook, so Retry and Fallback never see the stop and a run can go over by one turn. Subagents count toward the run, so delegation cannot spend around the cap. With Interrupt: true, a run-scoped budget pauses at its tool calls instead, and a BudgetDecision decides whether it continues:

for _, part := range resp.Interrupts() {
	if call, ok := middlewarex.BudgetInterrupted(part); ok {
		hold, _ := ai.InterruptAs[middlewarex.BudgetExceeded](call.Part) // which limit, how much was spent
		log.Println(hold.Message)
		parts = append(parts, call.Restart(middlewarex.BudgetDecision{Approved: true}))
	}
}

Meter it from outside

tracing.WithInstrumentation scopes instrumentation to a context, and aix.Observer reports each model call typed. Middleware and apps can count spend across nested calls without asking the app to register anything process-wide:

ctx = tracing.WithInstrumentation(ctx, aix.Observer{
	ModelDone: func(ctx context.Context, call *aix.ModelCall) {
		if u := call.Response.Usage; u != nil {
			meter.Add(tenant, call.Model, u)
		}
	},
})
out, err := supportAgent.RunText(ctx, msg)

A nested scope cannot remove an outer one, so an agent boundary never hides its subagents from an observer around it. Budget is built on this. go/samples/basic-middleware/metering bills two models at their own rates.

Long tool loops stay inside the context window

The experimental ContextCompression middleware compresses what the model receives once the context passes a trigger, cheapest strategy first: deduplicate repeated tool responses, truncate old ones, summarize older turns with a smaller model, then drop what is still over.

resp, err := genkit.Generate(ctx, g,
	ai.WithModelName("googleai/gemini-flash-latest"),
	ai.WithPrompt("Research the topic thoroughly."),
	ai.WithTools(search),
	ai.WithUse(
		&middlewarex.ContextCompression{
			MaxInputTokens:        80_000,
			DedupeToolResponses:   &middlewarex.CompressionDedupe{},
			TruncateToolResponses: &middlewarex.CompressionToolTruncation{MaxChars: 2000},
			Summarize: &middlewarex.CompressionSummarizer{
				Model: ai.NewModelRef("googleai/gemini-flash-lite-latest", nil),
			},
		},
		&middleware.Retry{}, // inside compression, so a retry resends the compressed view
	),
)

The model gets the compressed view, and resp.History() keeps every message, with the compression recorded in message metadata under contextCompression. That metadata is the whole state, so compression continues across calls, agent sessions, and restarts, and it is the same contract the JavaScript middleware writes: either runtime resolves a history the other compressed. Agent prompt scaffolding and output-format instructions are never compacted.

Agents that other Genkit apps serve

genkitx.DefineRemoteAgent connects to an agent that a Genkit app serves over HTTP, in Go, JavaScript, or Dart. It returns the same *aix.AgentHandle that LookupAgent returns for a local agent, and registers it by name, so it works as a sub-agent, background delegation included:

researcher := genkitx.DefineRemoteAgent(g, "researcher", "https://research.internal/agents/researcher",
	aix.WithHTTPClient(idTokenClient),
	aix.WithAgentMetadata(&aix.AgentMetadata{StateManagement: aix.AgentStateManagementServer, Abortable: true}),
)

genkitx.DefineAgent(g, "orchestrator",
	aix.InlinePrompt{
		ai.WithModelName("googleai/gemini-flash-latest"),
		ai.WithUse(&middlewarex.Agents{Agents: []aix.AgentRef{researcher.Ref()}, Async: true}),
	},
	aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()),
)

Turns stream over SSE, errors keep their status name, and RunDetached, Task, WaitForSnapshot, and Abort work over the companion routes. The client refuses redirects, never retries a turn, and sends the caller's traceparent. WithHeaders adds per-request headers, and WithRouteURLs points the companions elsewhere. The transport behind the handle is exported as aix.AgentTransport, so another protocol can plug in. go/samples/basic-agents-remote runs an orchestrator against a researcher in a second process.

The serving side changed to match:

  • Failed requests carry their status. A non-streaming failure from genkit.Handler is a JSON body, {"status":"INVALID_ARGUMENT","message":"..."}, the shape JS servers send, so statuses that share an HTTP code stay apart.
  • Served agents keep internal text in. Outside dev, an agent served through genkit.Handler sends the generic message for a non-public error, in turn results and in the snapshots its routes return. Errors about the caller's own input keep their text.
  • One wait request is bounded. aix.WithMaxSnapshotWait (default 25 seconds) caps a waitForSnapshot request, which then returns the row as it stands, so long waits pass through proxies. AgentHandle.WaitForSnapshot asks again until the row settles.
  • Task IDs are scoped to the conversation. The background-task tools accept only IDs that this call minted or that the history it sees carries, and Agents.MaxWaitSeconds caps how long the model can make the wait tool block.
  • Decoded errors match their sentinel. errors.Is(err, status.ErrNotFound) holds for a *status.Error read from JSON, a store, or a remote agent, so the same check works in process and across a wire.

Sessions in PostgreSQL

The experimental plugins/postgresql/exp package adds a session store with the Firestore store's fidelity: JSON Patch diffs anchored to a checkpoint every N turns, cross-process abort of detached runs over LISTEN/NOTIFY, metadata-only reads, and per-tenant prefixes. It shares the pool of the registered PostgreSQL plugin:

engine, err := postgresql.NewPostgresEngine(ctx,
	postgresql.WithCloudSQLInstance("my-project", "us-central1", "my-instance"),
	postgresql.WithDatabase("my-database"))
g := genkit.Init(ctx,
	genkit.WithExperimental(),
	genkit.WithPlugins(&postgresql.Postgres{Engine: engine}))

store, err := pgexp.NewPostgresSessionStore[ChatState](ctx, g,
	pgexp.WithSnapshotPathPrefix(tenantFromContext))

State is stored as json, which keeps exact text and accepts the NUL character tool output can carry, and a change to a row another row diffs against fails with FAILED_PRECONDITION instead of corrupting it. basic-agents-server runs on it with -store=postgres.

Prove a custom store with one call

ai/exp/sessionstoretest checks any aix.SessionStore against the contract the agent runtime relies on: chains, compaction, lifecycle writes, latest-row ordering, rich content, concurrent saves, and the optional capabilities. The bundled in-memory, file, Firestore, and PostgreSQL stores all run it.

func TestMyStore(t *testing.T) {
	sessionstoretest.Run(t, func(t *testing.T) aix.SessionStore[MyState] {
		return newEmptyStore(t)
	}, &sessionstoretest.Options[MyState]{
		Reopen: func(t *testing.T, s aix.SessionStore[MyState]) aix.SessionStore[MyState] {
			return reopen(t, s) // a second instance over the same storage
		},
		Scoped: func(t *testing.T) (aix.SessionStore[MyState], context.Context, context.Context) {
			return newTenantStore(t), tenantCtx("a"), tenantCtx("b")
		},
	})
}

Telemetry you plug in

Span instrumentation is pluggable, and OpenTelemetry is one provider of it rather than a dependency of the run. tracing.SetInstrumentation chooses the providers. A provider starts and ends spans, and Genkit owns the run, so no provider can skip or repeat it:

type printing struct{}

func (printing) StartSpan(ctx context.Context, info *tracing.SpanInfo) (context.Context, tracing.Span) {
	fmt.Printf("-> %s at %s\n", info.Name(), info.Path())
	return ctx, printingSpan{info.Name()}
}

type printingSpan struct{ name string }

func (printingSpan) TraceInfo() tracing.TraceInfo  { return tracing.TraceInfo{} } // Genkit mints local ids
func (printingSpan) SetMetadata(map[string]string) {}
func (s printingSpan) End(res *tracing.SpanResult) { fmt.Println("<-", s.name, res.Err()) }

tracing.SetInstrumentation(&tracing.OTelInstrumentation{}, printing{}) // keep the default, add your own

The default OTelInstrumentation creates spans on the TracerProvider the app registers. Genkit no longer installs a global one. The Dev UI is fed directly, with no OpenTelemetry SDK on that path, so the googlecloud and firebase plugins' otel.SetTracerProvider no longer cuts off Dev UI traces under genkit start. Write span metadata with tracing.SetSpanMetadata.

OpenTelemetry GenAI semantic conventions

The new otelgenai plugin reports Genkit actions with the OpenTelemetry GenAI semantic conventions, the counterpart of the JS @genkit-ai/otel plugin:

tracing.SetInstrumentation(otelgenai.New(&otelgenai.Options{
	ContentCapturingMode: otelgenai.SpanOnly, // default NoContent
	EmitToolSpans:        true,
}))
g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))

Model calls become chat <model> spans with gen_ai.* request, response, and usage attributes, plus the gen_ai.client.token.usage and gen_ai.client.operation.duration metrics. Message content is opt-in, on the span, on a log event, or both, and inline media is never captured. The app owns the SDK; go/samples/otel-genai wires it with autoexport. It replaces the default genkit:* encoding, so it is not for use with the googlecloud or firebase telemetry plugins.

The reflection API outside dev

The reflection API that the CLI and Dev UI talk to can run outside GENKIT_ENV=dev, and can require a secret:

GENKIT_REFLECTION_ENABLED=true \
GENKIT_REFLECTION_HOST=127.0.0.1 \
GENKIT_REFLECTION_PORT=3100 \
GENKIT_REFLECTION_SECRET_TOKEN=... \
./server

With a secret set, every request needs the x-genkit-reflection-secret header except /api/__health. GENKIT_REFLECTION_ENABLED=false keeps it off even under dev. genkit.WithReflectionPort picks the port in code, and 0 lets the OS choose. A chosen port is bound exactly, and an invalid setting panics from Init rather than falling back.

Decision models, in preview

The experimental plugins/systemone/exp plugin serves decision models: models that speak System One, such as TypeSafe's jev, Liquid AI's d1, and Cloudflare's Clef. A decision model evaluates a state against typed questions and returns calibrated answers in a few hundred milliseconds, for routing, classification, scoring, and guardrails. The output type is the question set:

type Triage struct {
	Department  systemonex.Choice[Dept] `json:"department" jsonschema_description:"Which team should handle this ticket?"`
	IsUrgent    systemonex.Noul         `json:"isUrgent" jsonschema_description:"Does the ticket explicitly communicate time pressure?"`
	Frustration systemonex.Score[Anger] `json:"frustration" jsonschema_description:"How frustrated is the customer?"`
}

g := genkit.Init(ctx, genkit.WithPlugins(systemonex.TypeSafe()))
out, _, err := genkit.GenerateData[Triage](ctx, g,
	ai.WithModelName("typesafe/jev-1.13.0"),
	ai.WithPrompt(ticket))
if out.Department.Confidence < 0.6 {
	return escalate(ticket)
}

The options of a choice come from Dept.Criteria() and the levels of a score from Anger.Levels(), so a question is defined once on its type. Constructors cover TypeSafe, OpenRouter's Decisions API, Cloudflare Workers AI, and Ollama, and &systemonex.SystemOne{...} serves any other server, with Route and Unwrap hooks for a different wire. Images, audio, and video go to models that read them, opted in per kind. A decision model also works as the ToolApproval judge. go/samples/basic-decisions shows a triage flow inline and from a prompt file.

Smaller things worth knowing

  • Skills conforms to the Agent Skills specification: bundled files through read_skill_file (AllowResourceAccess), Preload, ToolNamePrefix, an fs.FS source (SkillFS), and SkillToolName constants for allowlists. A symlinked or FIFO SKILL.md is refused (one read any file into the prompt, the other hung every call), a description cannot close the catalog block, and an unknown skill name answers the model instead of failing the run.
  • Fallback skips a model that failed for good (NOT_FOUND, PERMISSION_DENIED, UNAUTHENTICATED, INVALID_ARGUMENT, UNIMPLEMENTED) for the rest of the generate call, so a tool loop stops calling a dead primary every turn.
  • Tool definitions reach the model in declaration order. Go map order shuffled them on every call, which changed the request bytes and broke provider prompt caching.
  • ai.WithModel calls a model value directly, so a model built with ai.NewModelAction and never registered works in Generate and Prompt.Execute.
  • ai.MiddlewareFunc works in DefinePrompt and an agent's InlinePrompt. It failed every call with json: unsupported type.
  • Nested .prompt files are named by their path, support/main, and partials by theirs, {{> billing/system}}, so two subdirectories can each have a main.prompt or a _system.prompt. The short name still works for the first partial that claims it.
  • Ollama gains Headers, HTTPClient, and OLLAMA_API_KEY as a bearer token for hosted and proxied servers.
  • OpenAI-compatible providers send audio as input_audio and PDFs as file parts, and each preset lists the media types its chat API reads in OpenAICompatible.MediaTypes, so an unsupported part fails before the request. DeepSeek's empty reasoning_content is passed back, which DeepSeek requires for tool-call IDs it did not issue.
  • Gemini image models default ResponseModalities to text and image, so a bare prompt returns a picture. Vertex AI TTS detection no longer matches -tts in a project ID.
  • Non-streaming Anthropic requests that the SDK refuses for a large max_tokens are streamed and accumulated, so extended thinking works without a stream callback. This covers Claude on Model Garden too.
  • Streamed replies are stored as one text part rather than one part per chunk, and Gemini's end-of-stream thought signature is kept.
  • The MCP server sends each tool once, with its full input schema, and returns non-string output as JSON instead of fmt.Sprintf("%v"). StreamableHTTPConfig.HTTPClient is honored.
  • Outside dev, inline base64 media in span inputs and outputs is redacted to its data URL header. genkit:isFailureSource marks only the span a failure came from, so one failure is counted once. Status codes reach the telemetry server as OTLP values.
  • A client disconnect from genkit.Handler logs at debug, not error.
  • ResourceInput.Variables can be nil, NewDataPart(nil) stays a data part, and ListTools lists each tool once.
  • The Go test suite runs on Go 1.27 in CI.

Before you upgrade

One stable-surface break. postgresql.PostgresEngine.Close no longer closes a pool passed with WithPool; close it yourself. The same PR fixes IAM login for service accounts, credentials with spaces or quotes, a stray PGPASSWORD under IAM, and a leaked Cloud SQL dialer.

Removed. ai.NewToolInterruptError, exported only so middleware could hold a call, is gone; use tool.Interrupt(ctx, data). The experimental tool surface is deleted now that the stable one covers it:

Experimental Stable
github.com/firebase/genkit/go/ai/exp/tool github.com/firebase/genkit/go/ai/tool
genkitx.DefineInterruptibleTool, *aix.InterruptibleTool genkit.DefineResumableTool, *ai.ResumableToolAction
genkitx.DefineTool genkit.DefineTool
tool.Interrupt(data) tool.Interrupt(ctx, data)
tool.InterruptAs[T](part) ai.InterruptAs[T](part)
myTool.Resume(part, res), myTool.Respond(part, out) myTool.Interrupted(part), then call.Restart(res) or call.Respond(out)
tool.SendPartial tool.SendChunk with a RoleTool chunk

Deprecated, each naming its replacement in godoc: ToolContext.Interrupt, ai.InterruptWith, RestartWith, RespondWith, Tool.Restart, Tool.Respond, WithToolRestarts, WithToolResponses (use WithResume), WithNewInput, WithResumedMetadata, WithResponseMetadata, ai.IsToolResumed, ai.ResumedValue (use tool.ResumeData), ai.OriginalInputAs (use tool.OriginalInput), ai.NewMultipartTool and genkit.DefineMultipartTool (use tool.AttachParts), and tracing.TracerProvider, tracing.Tracer, tracing.WriteTelemetryImmediate, WriteTelemetryBatch, and WriteTelemetryRealtime (use tracing.SetInstrumentation).

Dependency floors rise. OpenTelemetry moves from v1.36.0 to v1.46.0, and go.opentelemetry.io/otel/log v0.22.0 becomes a requirement. Stable OTel minors are compatible, but otel/log is pre-1.0 and v0.21.0 removed log.KeyValue and log.Value, so a log bridge built against v0.20 or earlier must upgrade. gRPC, google.golang.org/api, Firestore, BigQuery, cloud.google.com/go/auth, and the GCP detector floors rise with it.

Numbers and runtime behavior that change:

  • Usage reported under one convention moves dashboards: OpenAI, DeepSeek, and OpenRouter OutputTokens drop by the reasoning count, Anthropic InputTokens rise by cache reads and writes while OutputTokens drop by thinking, and Gemini InputTokens include tool-use prompt tokens.
  • The Dev UI shows Genkit spans only. Spans an app writes through OpenTelemetry directly (otelhttp, trace.SpanFromContext(ctx).SetAttributes) no longer reach it, and without an SDK registered, OTel spans are no-ops. Only genkit.Init reads GENKIT_TELEMETRY_SERVER, in dev or with reflection enabled.
  • A failed non-streaming request served by genkit.Handler has a JSON body, so a JS client calling a Go server shows {"status":"INVALID_ARGUMENT","message":"invalid argument"} as the error text.
  • Any restart of a ToolApproval hold without "toolApproved": true refuses the call; it used to hold it again. {"toolApproved": true} works as before.
  • A resume that pauses again returns (resp, nil) with FinishReason interrupted, where it returned FAILED_PRECONDITION beside the response.
  • A restart that revises a tool's input changes the request in history. WithResume() with no parts, or Resume: {} over the wire, resumes a conversation that ends on tool requests. Definition().Metadata carries "resumeSchema" for every tool.
  • part.Metadata["interrupt"] is nil on a part that the loop or JSON decoding made; read interrupts with IsInterrupt and InterruptAs.
  • Filesystem answers failed file operations under the SoftToolErrors rules, so cancellation and errors from other hooks stop the generation. An MCP tool result with isError reaches the model as a tool error.
  • OLLAMA_API_KEY, when set, is sent as a bearer token to any Ollama host that has no Authorization header.
  • In ai/exp: a prompt agent fails a turn whose response was blocked (ai.ErrGenerationBlocked) or rejected by its output schema, and the failed row holds the request's messages, not the rejected answer; a direct HTTP client of waitForSnapshot must loop on a pending or aborting row; a repeated agent or store option replaces the earlier one instead of panicking; aix.Diff and aix.ApplyPatch return numbers as json.Number; and an abort that lands as a turn ends settles as completed or failed by how the work ended.

What's Changed

  • fix(go/ai/exp): settle an aborting snapshot by how its work ended by @apascal07 in #6311
  • fix(go/plugins/mcp): honor StreamableHTTPConfig.HTTPClient in the Str… by @vllst-io in #6339
  • fix(go/ai/exp): honor abort in conformance fixture by @michaelhvisser in #6415
  • docs(cli,js,go,python): add coding agent skill installation instructions to READMEs by @pavelgj in #6487
  • feat(go/plugins/middleware): conform Skills to the Agent Skills specification by @apascal07 in #6219
  • feat(go/plugins/typesafe): add TypeSafe's jev as a decision model, in preview by @apascal07 in #6426
  • test(go): fix abort flakes in the agent tests by @apascal07 in #6611
  • fix(go/ai/exp): tag a committed failed turn's snapshot ID on its turn span by @apascal07 in #6425
  • feat(cli): optional reflection API auth (--experimental-auth), loopback-only v2 server by @pavelgj in #6453
  • feat(go): return tool errors to the model with tool.Fail and SoftToolErrors by @apascal07 in #6431
  • fix(go/ai): advertise a tool's own output schema, never the multipart envelope by @apascal07 in #6440
  • fix(go/ai): check a resume as a whole before any tool runs by @apascal07 in #6441
  • feat(go/ai): carry interrupt and restart state in typed Part fields by @apascal07 in #6442
  • feat(go/ai/tool): add the tool runtime verbs, scoped to each tool call by @apascal07 in #6443
  • feat(go/ai): answer a restart at the stage that raised the interrupt by @apascal07 in #6444
  • feat(go/ai): add resumable tools resolved through claimed calls by @apascal07 in #6445
  • refactor(go): remove the experimental tool surface by @apascal07 in #6446
  • feat(go/plugins/middleware): let a judge model decide ToolApproval calls by @apascal07 in #6451
  • feat(go): decouple reflection API from dev mode, add optional auth by @pavelgj in #6455
  • fix(go): report GenerationUsage under one token convention across model plugins by @apascal07 in #6614
  • chore(go,py,js): add total usage and agent usage to the shared schemas by @apascal07 in #6435
  • feat(go/ai): sum model usage across the tool loop into TotalUsage by @apascal07 in #6436
  • feat(go/ai/exp): report token usage per agent turn, invocation, and session by @apascal07 in #6437
  • feat(go/ai,go/plugins/middleware): claim and answer middleware holds through InterruptedCall by @apascal07 in #6678
  • fix(go/ai): report an interrupt during a resume as a pause, not an error by @apascal07 in #6677
  • ci(go): run the Go test suite on Go 1.27 by @apascal07 in #6719
  • docs(go): trim the README to a feature overview by @apascal07 in #6722
  • test(go/plugins): add a shared live test suite for model plugins by @apascal07 in #6656
  • test(go/plugins/anthropic): run the live tests on the shared suite by @apascal07 in #6657
  • test(go/plugins/googlegenai): run the live tests on the shared suite by @apascal07 in #6658
  • test(go/plugins/ollama): run the live tests on the shared suite by @apascal07 in #6659
  • fix(go): resolve the defects the shared live suite found by @apascal07 in #6667
  • feat(go/plugins/middleware): add ContextCompression middleware by @apascal07 in #6723
  • fix(go/genkit): list each tool once in ListTools by @apascal07 in #6727
  • fix(go/samples): exit cleanly when server.Start returns nil by @apascal07 in #6730
  • fix(go/plugins/googlegenai): name real Genkit APIs in config validation errors by @apascal07 in #6729
  • fix(go/ai): accept nil Variables in ResourceInput by @apascal07 in #6728
  • fix(go/ai): recover panics in tool calls by @apascal07 in #6731
  • fix(go/plugins/middleware): skip dead models for the rest of a Fallback call by @apascal07 in #6733
  • fix(go/plugins/mcp): send full tool schemas and JSON results from the MCP server by @apascal07 in #6734
  • fix(go/genkit): log client disconnects at debug, not error by @apascal07 in #6736
  • fix(go/ai): call model values passed to WithModel without a registry lookup by @apascal07 in #6737
  • fix(go/plugins): merge streamed text parts and keep Gemini stream signatures by @apascal07 in #6739
  • fix(go/ai): allow ai.MiddlewareFunc in prompt and agent options by @apascal07 in #6738
  • fix(go/plugins/anthropic): stream requests the SDK refuses as non-streaming by @apascal07 in #6735
  • fix(go/ai/exp): deliver a failed turn's TurnEnd before the output resolves by @apascal07 in #6732
  • fix(go/plugins/localvec): wrap embedder errors with %w by @apascal07 in #6742
  • fix(go/plugins/a2ui): keep thought signatures on rebuilt text parts by @apascal07 in #6740
  • fix(go/plugins/compat_oai): send audio and PDF input as input_audio and file parts by @apascal07 in #6741
  • refactor(go/plugins/systemone): split the System One protocol out of the TypeSafe plugin by @apascal07 in #6603
  • refactor(go/plugins/systemone): prepare the wire code for servers other than TypeSafe's by @apascal07 in #6604
  • feat(go/plugins/systemone): serve any System One server with one plugin by @apascal07 in #6605
  • feat(go/plugins/systemone): add OpenRouter's Decisions API as a server by @apascal07 in #6606
  • feat(go/plugins/systemone): send media parts as the request's images, audio, and videos by @apascal07 in #6607
  • feat(go/plugins/systemone): add Cloudflare Workers AI as a server by @apascal07 in #6608
  • docs(go/plugins/firebase/exp): fix the package example that does not compile by @apascal07 in #6752
  • fix(go/core/status): match a decoded error against its status's base sentinel by @apascal07 in #6754
  • fix(go/core): redact inline base64 media from span attributes outside dev by @mikemikimike in #5967
  • fix(go/ai): send tool definitions in declaration order by @Anthony-Bible in #5334
  • feat(go/plugins/ollama): add Headers and HTTPClient for hosted servers by @a2105z in #5946
  • fix(go/core/tracing): send OTLP status codes to the telemetry server by @hahahahahayesyeseys in #6389
  • fix(go/tracing): attribute a failure only to the span it originates from by @as992949791 in #6477
  • fix(go/ai): preserve data parts with null payloads by @CSXizhang in #6541
  • fix(go/ai,js/ai,py): name nested dotprompt files by their subdirectory by @justinakkal in #5410
  • fix(go/plugins/googlegenai): default ResponseModalities for Gemini image models by @apascal07 in #6789
  • fix(go/plugins/compat_oai): pass back empty reasoning_content by @apascal07 in #6786
  • feat(go/ai/exp): add sessionstoretest, a conformance suite for session stores by @apascal07 in #6747
  • fix(go/plugins/postgresql)!: fix engine connection defects and leave a caller's pool open by @apascal07 in #6812
  • feat(go/plugins/postgresql/exp): add a PostgreSQL session store by @apascal07 in #6751
  • feat(go): align agent waits, rejected turns, and task scoping with the JS agent train by @apascal07 in #6746
  • refactor(go/ai/exp): let a repeated agent option replace the earlier one by @apascal07 in #6748
  • fix(go/genkit): send structured errors and keep internal text out of served agents by @apascal07 in #6749
  • feat(go/ai/exp): call agents that another Genkit app serves by @apascal07 in #6750
  • feat(go): add pluggable trace instrumentation and decouple it from OpenTelemetry by @pavelgj in #6343
  • feat(go/plugins/otelgenai): add OpenTelemetry GenAI instrumentation and upgrade otel to v1.46.0 by @pavelgj in #6345
  • feat(go/core/tracing,go/ai/exp): scope instrumentation to a context and observe model calls with it by @apascal07 in #6717
  • feat(go/plugins/middleware/exp): add Budget to cap run and session usage by @apascal07 in #6612
  • fix(go/ai): keep the request out of a model action's output by @apascal07 in #6832

Full Changelog: go/v1.13.1...go/v1.14.0

Don't miss a new genkit release

NewReleases is sending notifications on new releases.