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.0Tools 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 includedfor 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 itTotalUsage 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.Handleris 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.Handlersends 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 awaitForSnapshotrequest, which then returns the row as it stands, so long waits pass through proxies.AgentHandle.WaitForSnapshotasks 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.MaxWaitSecondscaps how long the model can make the wait tool block. - Decoded errors match their sentinel.
errors.Is(err, status.ErrNotFound)holds for a*status.Errorread 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 ownThe 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=... \
./serverWith 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
Skillsconforms to the Agent Skills specification: bundled files throughread_skill_file(AllowResourceAccess),Preload,ToolNamePrefix, anfs.FSsource (SkillFS), andSkillToolNameconstants for allowlists. A symlinked or FIFOSKILL.mdis 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.Fallbackskips 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.WithModelcalls a model value directly, so a model built withai.NewModelActionand never registered works inGenerateandPrompt.Execute.ai.MiddlewareFuncworks inDefinePromptand an agent'sInlinePrompt. It failed every call withjson: unsupported type.- Nested
.promptfiles are named by their path,support/main, and partials by theirs,{{> billing/system}}, so two subdirectories can each have amain.promptor a_system.prompt. The short name still works for the first partial that claims it. - Ollama gains
Headers,HTTPClient, andOLLAMA_API_KEYas a bearer token for hosted and proxied servers. - OpenAI-compatible providers send audio as
input_audioand PDFs asfileparts, and each preset lists the media types its chat API reads inOpenAICompatible.MediaTypes, so an unsupported part fails before the request. DeepSeek's emptyreasoning_contentis passed back, which DeepSeek requires for tool-call IDs it did not issue. - Gemini image models default
ResponseModalitiesto text and image, so a bare prompt returns a picture. Vertex AI TTS detection no longer matches-ttsin a project ID. - Non-streaming Anthropic requests that the SDK refuses for a large
max_tokensare 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.HTTPClientis honored. - Outside dev, inline base64 media in span inputs and outputs is redacted to its data URL header.
genkit:isFailureSourcemarks 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.Handlerlogs at debug, not error. ResourceInput.Variablescan be nil,NewDataPart(nil)stays a data part, andListToolslists 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
OutputTokensdrop by the reasoning count, AnthropicInputTokensrise by cache reads and writes whileOutputTokensdrop by thinking, and GeminiInputTokensinclude 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. Onlygenkit.InitreadsGENKIT_TELEMETRY_SERVER, in dev or with reflection enabled. - A failed non-streaming request served by
genkit.Handlerhas 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
ToolApprovalhold without"toolApproved": truerefuses the call; it used to hold it again.{"toolApproved": true}works as before. - A resume that pauses again returns
(resp, nil)withFinishReasoninterrupted, where it returnedFAILED_PRECONDITIONbeside the response. - A restart that revises a tool's input changes the request in history.
WithResume()with no parts, orResume: {}over the wire, resumes a conversation that ends on tool requests.Definition().Metadatacarries"resumeSchema"for every tool. part.Metadata["interrupt"]is nil on a part that the loop or JSON decoding made; read interrupts withIsInterruptandInterruptAs.Filesystemanswers failed file operations under theSoftToolErrorsrules, so cancellation and errors from other hooks stop the generation. An MCP tool result withisErrorreaches the model as a tool error.OLLAMA_API_KEY, when set, is sent as a bearer token to any Ollama host that has noAuthorizationheader.- 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 ofwaitForSnapshotmust loop on apendingorabortingrow; a repeated agent or store option replaces the earlier one instead of panicking;aix.Diffandaix.ApplyPatchreturn numbers asjson.Number; and an abort that lands as a turn ends settles ascompletedorfailedby how the work ended.
What's Changed
- fix(go/ai/exp): settle an
abortingsnapshot 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
Skillsto 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.FailandSoftToolErrorsby @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
Partfields 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
ToolApprovalcalls by @apascal07 in #6451 - feat(go): decouple reflection API from dev mode, add optional auth by @pavelgj in #6455
- fix(go): report
GenerationUsageunder 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
TotalUsageby @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
InterruptedCallby @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
ContextCompressionmiddleware by @apascal07 in #6723 - fix(go/genkit): list each tool once in
ListToolsby @apascal07 in #6727 - fix(go/samples): exit cleanly when
server.Startreturns 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
VariablesinResourceInputby @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
Fallbackcall 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
WithModelwithout 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.MiddlewareFuncin 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
TurnEndbefore the output resolves by @apascal07 in #6732 - fix(go/plugins/localvec): wrap embedder errors with
%wby @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_audioandfileparts 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
HeadersandHTTPClientfor 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
ResponseModalitiesfor Gemini image models by @apascal07 in #6789 - fix(go/plugins/compat_oai): pass back empty
reasoning_contentby @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
Budgetto 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