feat(go): added common middleware (e.g. tool approval, retry, fallback)#4719
Merged
feat(go): added common middleware (e.g. tool approval, retry, fallback)#4719
Conversation
Co-authored-by: Pavel Jbanov <[email protected]>
Co-authored-by: Pavel Jbanov <[email protected]>
…into ap/go-middleware
Rework the Go middleware primitives introduced in PR #4464 to collapse configuration and behavior into a single "config struct is the middleware" model and remove the descriptor/factory/prototype scaffolding. - Drop the Middleware interface (Name/New/WrapGenerate/WrapModel/WrapTool/Tools) and the BaseMiddleware embedding helper. Introduce Hooks as a plain struct of optional hook func fields (WrapGenerate, WrapModel, WrapTool, Tools); nil hooks pass through. - Repurpose Middleware as an interface with just Name() + New(ctx), which a user-facing config struct implements directly. Passing a config value to WithUse runs its New on the local fast path with no registry lookup, so pure-Go code works without plugin registration. - NewMiddleware[M](description, prototype) captures the typed prototype in a closure stored on MiddlewareDesc.buildFromJSON, preserving unexported plugin-level state across JSON-dispatched calls via value-copy. - MiddlewareDesc returns to being the shared schemas.config-generated type with the private factory added via the existing `field` directive. - Rename MiddlewarePlugin.ListMiddleware to Middlewares to align with the upcoming V2 naming conventions. - Replace Inline with MiddlewareFunc, a canonical Go adapter type that satisfies Middleware for ad-hoc closure-based middleware. - Add genkit.DefineMiddleware and genkit.LookupMiddleware wrappers with complete godoc matching the DefineTool/LookupTool style. Fixes carried over from the initial review: - Preserve MultipartToolResponse.Content through the resume path in handleResumedToolRequest (previously dropped). - Change WrapTool return type to *MultipartToolResponse so metadata and content flow through without an out-of-band capture hack. - Reject duplicate middleware-contributed tool names explicitly in GenerateWithRequest instead of panicking at registry registration. - Build the WrapGenerate, WrapModel, and WrapTool hook chains once per GenerateWithRequest rather than rebuilding them on every tool-loop turn. - Export NewToolInterruptError so WrapTool hooks can interrupt tools without constructing a ToolContext. Tests rewritten against the new shape and expanded to cover: plugin-state value-copy, call-level state isolation, MiddlewareFunc adapter, nil hooks, stream chunk accumulation, tool contribution, duplicate-tool rejection, factory error propagation, WrapTool interrupts, per-iteration WrapGenerate, and metadata round-trip through WrapTool. All green under -race.
Adopt the new Hooks-based middleware architecture and update the built-in middleware implementations (Retry, Fallback, ToolApproval, Filesystem, Skills) to match. - Replace ai.BaseMiddleware embedding + WrapX methods with New(ctx) (*ai.Hooks, error) returning a per-call hooks bundle - Switch WrapTool to return *ai.MultipartToolResponse (preserves Content/Metadata through the resume path) - Move Filesystem's per-call queue + os.Root open into the closure returned from New(); tools are now exposed via Hooks.Tools - Skills now scans paths inside New() and returns Tools via Hooks.Tools - Rename plugin's ListMiddleware -> Middlewares to match the new ai.MiddlewarePlugin interface Also re-applies the streaming format handler fix from this branch on top of the new generate.go pipeline so middleware-emitted chunks share the model's accumulator. Tests pass under -race.
# Conflicts: # go/ai/generate.go # go/ai/middleware_test.go
pavelgj
reviewed
Apr 21, 2026
huangjeff5
reviewed
Apr 21, 2026
pavelgj
approved these changes
Apr 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a
middlewareplugin that bundles five production-ready implementations of theai.Middlewareinterface introduced in #4464:Retrywith exponential backoff,Fallbackacross models,ToolApprovalfor human-in-the-loop gating,Filesystemfor scoped file access, andSkillsfor loadableSKILL.mdpersonas.Examples
Composing Retry and Fallback
Register the
middlewareplugin duringInitto expose the built-ins to the Dev UI, then attach them withai.WithUse. Middleware composes outer-to-inner, soai.WithUse(&Retry{...}, &Fallback{...})expands toRetry { Fallback { model } }: each model in the fallback list is retried with exponential backoff before the next fallback is tried.Tool Approval
ToolApprovalinterrupts any tool call outside itsAllowedToolslist. The caller approves (or rejects) and resumes withai.WithToolRestarts, reusing the existing interrupt/restart machinery:Filesystem
Filesystemregisterslist_files,read_file, and optionallywrite_fileandsearch_and_replace, all confined toRootDir. Path safety is enforced byos.Root(Go 1.25+), which rejects any path that resolves outside the root, including via.., absolute paths, or symlinks:Skills
Skillsexposes a local library of loadable instructions. Each skill is a directory with aSKILL.mdfile; the middleware advertises available skills (name plus optional description) in the system prompt and registers ause_skilltool that loads the chosen skill's full body into the conversation on demand:API Reference
Built-in middleware (
plugins/middleware)Each type satisfies the simplified
ai.Middlewarecontract from #4464 by implementingName() stringandNew(ctx) (*ai.Hooks, error).Newreturns a per-call hook bundle (Tools,WrapGenerate,WrapModel,WrapTool); per-call state — likeFilesystem's message queue and resolvedos.Root, orSkills' scanned skill set — lives in closures captured byNewso concurrent calls stay isolated.Notes
Middlewareinterface from feat(go/ai): addedDefineMiddleware(Middleware V2) #4464 (config struct +New(ctx) (*ai.Hooks, error); tools surfaced viaHooks.Tools).Filesystemdepends onos.Root).go/samples/basic-middleware/:retry-fallback,filesystem, andskills.plugins/googlegenai, a streaming ordering fix that preservesgenerate > model > toolturn order when resuming restarted tools, a refactor of theFallback/Retrysplit into separate middlewares, and a streaming-format-handler fix so chunks emitted fromWrapGenerateaccumulate alongside model-emitted chunks.