Uh oh!
There was an error while loading. Please reload this page.
feat(go): added experimental ways to define tools, interrupts, and streaming - #4797
Merged
Conversation
Resolve conflict in go/samples/basic-agents/main.go: keep the four-agent (banker + hooks/agentEntry) description from ap/go-x-tools while adopting the StreamBidi -> Connect rename from ap/go-session-flow.
decodeInit decoded the JSON init payload straight into the typed Init, so any field the InitSchema did not declare was silently dropped before validation ever saw it: an agent could be started with a misspelled or bogus init field and get a fresh session with no error. Route init through base.UnmarshalAndNormalize against the resolved InitSchema, the same pipeline the unary input path uses, so the raw payload is validated (additionalProperties rejects unknown fields) and normalized (e.g. integer widening) before the typed decode. Add coverage for unknown-field rejection on ConnectJSON/RunBidiJSON at both the core BidiAction layer and the agent layer (real inferred AgentInit schema), plus a guard that init is normalized, not just validated.
Stamp the agent's root action span with the session ID under "genkit:metadata:agent:sessionId" so traces from the same conversation can be correlated. This mirrors the JS agent (PR #5251), which sets the attribute once on the action span via setCustomMetadataAttributes; Go has no such helper, so the genkit:metadata: prefix is inlined. Align the per-turn span shape with JS's run() too: name "runTurn-N" (1-indexed) with type flowStep and no subtype, replacing "agent/turn/N".
Add two construction options to localstore.FileSessionStore, mirroring the JS FileSessionStore (PR #5251): - WithMaxPersistedChainLength(n): on each save, walk the new snapshot's parentId chain and unlink rows past the newest n, capping per-conversation disk use. n must be >= 1; 0 or negative is rejected. - WithSnapshotPathPrefix(fn): derive a per-call subdirectory from context for tenant isolation. The prefix may nest via "/" and is sanitized against directory escape. Snapshots remain flat by id within the prefix (<dir>/<prefix>/<snapshotId>.json), so resume-by-snapshot, heartbeat, finalize, abort, and status subscription stay O(1) direct opens, while GetLatestSnapshot scans the prefix directory. With no options configured the on-disk layout is unchanged. Options follow the ai/option.go interface pattern (in a new option.go) and error if set more than once.
…tore FileSessionStore.OnSnapshotStatusChange previously reflected only status changes written through the same store instance, so a detached turn running in one process could not be aborted by another process sharing the directory. Add an internal poller that re-reads subscribed snapshot files on an interval (default 1s, configurable via WithPollInterval; <=0 disables it) and delivers any change. The instant in-process fast path is kept for same-process delivery; both paths funnel through a single per-snapshot dedup gate, so a change is delivered exactly once regardless of which observes it first. The poller is started lazily on the first subscriber and stopped when the last unsubscribes, so a store with no subscriptions pays nothing. Polling (rather than fsnotify) matches the SnapshotSubscriber contract, adds no dependency, and is immune to the temp-file+rename inode swap.
…pt/NamedPrompt Replace the FromInline/FromPrompt agent-source constructors with three clearer forms: - InlinePrompt(opts...) defines the prompt inline (was FromInline) - SameNamedPrompt() references the prompt named like the agent - NamedPrompt(name, input) references any registered prompt by name, rendered with an input supplied from code NamedPrompt decouples the prompt's lookup name from the agent's name, so a single prompt can back many agents with different inputs. The old FromPrompt(defaultInput...) variadic-of-one is gone; per-turn input now rides on NamedPrompt. Updates the wrapper docs, both samples (chef's personality moves to the .prompt frontmatter default), the README, and the tests, and adds TestPromptAgent_NamedPromptSharedAcrossAgents covering the shared-prompt path. Breaking change to the experimental ai/exp API.
pavelgj
approved these changes
Jun 23, 2026
Split the prompt-backed agent constructors so each has one clear job: - DefineAgent(r, name, prompt, opts...) defines the prompt inline; the prompt is an InlinePrompt, a []ai.PromptOption slice passed positionally - DefinePromptAgent(r, name, opts...) sources a prompt from the registry, defaulting to the agent's own name DefinePromptAgent uses the same-named prompt by default; WithNamedPrompt(name, input) points it at a different registered prompt rendered with a code-supplied input, so one prompt can back many agents. The prompt source is split across a compile-time-validated option set mirroring ai/option.go: the shared options (WithSessionStore, WithStateTransform, WithDescription) are AgentOptions valid on every constructor, while WithNamedPrompt is a PromptAgentOption accepted only by DefinePromptAgent. Passing it to DefineAgent or DefineCustomAgent fails to compile. Making the inline prompt a required positional argument means an inline agent cannot be defined without one. Removes the AgentSource abstraction and the SameNamedPrompt/NamedPrompt sources. Updates the wrapper docs, both samples, and the tests. Breaking change to the experimental ai/exp API.
The "Load the Prompt from a File" path now uses DefinePromptAgent (default same-named lookup) with WithNamedPrompt for shared prompts, and the inline example uses the InlinePrompt slice literal.
…pans
Each runTurn-N span now records the committed session state at turn end as
its genkit:output, shaped as {state: <session state>}, and for
server-managed agents carries the turn-end snapshot's ID under
genkit:metadata:agent:snapshotId. The session ID stays on the root action
span as before.
The state is raw: StateTransform shapes only client-facing surfaces, not
telemetry or persisted state, so the span output matches the snapshot its
ID points to.
With the turn span output now derived from session state, the per-turn
chunk collection is gone: removed SessionRunner.collectTurnOutput,
chunkRouter.collectTurnChunks and its turnChunks/turnMu fields, and the
accumulation branch in applySideEffects (now artifact-only).…rror WithStreamTransform is the stream-side counterpart to WithStateTransform, rewriting each AgentStreamChunk on its way to the client. Both StateTransform and StreamTransform now return (value, error): a nil value omits the state or drops the chunk (wire-only), while a non-nil error (or a panic) fails the read or invocation closed with the transform's status preserved. Updates go/README.md and the genkit.DefineAgent option docs.
The abortSnapshot companion action's response carried only status, so a caller could not correlate the result with the snapshot it aborted. Add snapshotId (matching the abortSnapshot request) and populate it from the request. Authored in the shared zod schema (genkit-tools/common/src/types/agent.ts) and regenerated across genkit-schema.json, the Go bindings (go/ai/exp/gen.go), and the Python typings (_typing.py); the Go doc and noomitempty come from schemas.config.
The operation aborts a turn by marking its pending snapshot aborted for the
active turn to observe; it does not abort a snapshot. Name it for that
turn-level intent.
- Agent.AbortSnapshot -> Agent.Abort; AbortSnapshotAction -> AbortAction
- HTTP route POST /agents/{name}/abortSnapshot -> /agents/{name}/abort
- Wire types AbortSnapshotRequest/Response -> AgentAbortRequest/Response,
regenerated into genkit-schema.json, go gen.go, and py _typing.py
The unexported abortPendingSnapshot helper keeps its name: it names the
mechanism (flip the pending snapshot to aborted), distinct from the
turn-level abort.
BREAKING CHANGE: the Agent.Abort* Go API, the /abort HTTP route, and the
AgentAbort{Request,Response} schema types (Go, Python, JS) replace the
former AbortSnapshot* names.A per-turn handler could not learn its snapshot ID until after the turn (on the TurnEnd chunk), so durable agents had no way to name snapshot-correlated external resources (e.g. a git worktree) before doing the turn's work. Reserve the turn's snapshot ID at turn start (the runtime now mints it rather than the store) and surface it, the parent snapshot ID, and the turn index on a TurnContext. The turn-end snapshot persists under that reserved ID, so the ID the handler reads up front is the ID the snapshot lands under. The TurnContext rides on the per-turn fn's context.Context (TurnContextFromContext) rather than the SessionRunner.Run callback signature, so existing custom agents compile unchanged. Empty SnapshotID for client-managed agents and for turns that write no snapshot. The detach pending row stays store-minted.
…nkit/exp The experimental Define* surface (DefineXTool, DefineInterruptibleTool, DefineAgent, DefinePromptAgent, DefineCustomAgent, ListAgents) moves out of the stable genkit package into genkit/exp, alongside the agent route builders and DefineStreamingFlow already there. The package qualifier lets DefineXTool drop the X workaround and become exp.DefineTool without colliding with the stable genkit.DefineTool. The registry stays internal: genkit/exp reaches it through a new internal/genkitbridge hook that genkit installs in its init, so no public registry accessor is added and external code cannot import the bridge. ListAgents is reimplemented over the bridge. Agent-handler serving tests move to an external genkit_test package to avoid the genkit -> genkit/exp import cycle. Samples and go/README.md are updated to the genkitx.Define* surface. BREAKING CHANGE: genkit.DefineXTool, genkit.DefineInterruptibleTool, genkit.DefineAgent, genkit.DefinePromptAgent, genkit.DefineCustomAgent, and genkit.ListAgents are removed in favor of the genkit/exp equivalents. These APIs are experimental and unshipped.
PR #4462 squash-landed the agent foundation that this branch also builds, so most conflicts were add/add on identical content. Resolution: - 23 add/add files differed only by copyright year (2025 vs 2026); took main's 2026 copies. - go/ai/generate.go: kept the branch's nil-guard on Interrupts() and the new ToolResponses() method (pure additions; main had neither). - go/genkit/exp/*, README, and basic-agents samples: kept the branch's design (experimental definers in genkit/exp; the banker tool-interrupt/resume demo). - go/genkit/servers_test.go: dropped TestHandlerAgent/TestHandlerAgentRef; their coverage moved to go/genkit/exp/routes_test.go when DefineAgent left package genkit. Verified: go build ./..., go vet ./..., and tests for go/genkit, go/genkit/exp, go/ai/exp, go/ai, go/core all pass.
wrapSimpleFunc and wrapInterruptibleFunc duplicated their entire body (parts-collector context setup, original-input plumbing, interrupt-error conversion, and multipart-response assembly); only the user-function call differed. Extract that scaffolding into a shared runToolFunc helper so the two wrappers stay thin adapters. Behavior-preserving: the interruptible wrapper's MapToStruct error now routes through convertInterruptError, which is a no-op for non-InterruptError values.
…l tool APIs Tools run on concurrent goroutines, so tool.SendPartial / tool.SendChunk could race on the wrapped stream callback (shared role/index state and the single sink). Route every tool-originated send through one mutex in handleToolRequests; log and drop best-effort sink errors and guard the chunk type assertion against a stray value. Add the missing test coverage for the experimental tool surface: - ai/exp/tool: unit tests for Interrupt/Resume/Respond/InterruptAs/AttachParts/ OriginalInput/SendPartial/SendChunk. - ai/exp: integration over DefineTool/DefineInterruptibleTool including the typed interrupt/resume round-trip and a -race regression for the streaming serialization above. - ai: units for NewPartialToolResponsePart/IsPartial, ToolResponses, and the corrected ModelResponseChunk.Text behavior. Interrupt and resume payloads must serialize to a JSON object: return a clear, typed error instead of an opaque json failure, and document the constraint on the relevant helpers. Bump touched-file copyright headers to 2026.
Uh oh!
There was an error while loading. Please reload this page.
apascal07 added a commit
that referenced
this pull request
Jun 25, 2026
…ddleware Resolve conflicts from main absorbing the agent foundation (#4462, #4797, #4387, #5576) via separate squashed PRs: - Adopt main's renames (AgentAbort{Request,Response}, Agent.Abort/AbortAction, abort routes) and its turn-context feature across go/ai/exp + schema. - Preserve this branch's net-new middleware surface: ArtifactStore, AgentRef, WithContextFunc context seeding, and the orchestrator sample. Per review direction, consolidate the agent constructors into the genkit/exp (genkitx) surface that main migrated docs/samples/tests to: - Move the genkit-instance seeding into genkitx.DefineAgent/DefinePromptAgent/ DefineCustomAgent via a new genkitbridge.SeedContext hook, so middleware resolves sub-agents through the documented exp constructors. - Remove the now-dead genkit.DefineAgent/DefinePromptAgent/DefineCustomAgent/ ListAgents duplicates from package genkit. - Reconcile the basic-agents sample to keep both the banker (interrupt/resume) and orchestrator (middleware delegation) demos on genkitx.
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 freeto 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 experimental tool definition APIs (
ai/exp) with simplified function signatures, typed interrupt/resume, and a runtime helper package for use inside tool functions.These APIs are designed to replace the existing
DefineToolandDefineMultipartToolin a future major release. The current implementation wraps the existing tool infrastructure for backwards compatibility, but the intent is to invert this -- the new APIs will become the primary implementation and the old ones will be removed.Examples
Simple Tools
The new experimental tool constructor (
genkitx.DefineTool, in thegenkit/exppackage) replaces both the stableDefineToolandDefineMultipartToolwith a single API. The function receives a plaincontext.Contextinstead ofai.ToolContext, making tool functions easier to write and test. Multipart responses are handled viatool.AttachPartsrather than requiring a separate function signature:Use
tool.AttachPartsto return additional content (images, media) without changing the function signature:Partial Tool Responses (Streaming Progress)
Tools can stream partial responses during execution for client-side display (e.g., progress indicators). Use
tool.SendPartialinside any tool function -- it's best-effort and no-ops when streaming isn't available.Define a status type shared between the tool and client:
Send partial responses from the tool:
On the client side, partial tool responses arrive as
ModelResponseChunks during streaming. UsePart.IsPartial()to distinguish progress updates from final tool results:Interruptible Tools
DefineInterruptibleTooladds typed interrupt/resume for human-in-the-loop workflows. The function receives a*Resparameter that is non-nil when the tool is being re-executed after an interrupt. LikeDefineTool, multipart responses are supported viatool.AttachParts:Handle the interrupt on the caller side. Use
transferTool.Resumefor typed, compile-time-checked resume data:Or respond with a pre-computed result instead of re-executing the tool:
When the caller does not have access to the tool definition (e.g., in a generic interrupt handler), use the definition-free helpers
tool.Resumeandtool.Respond:Interrupts in an Agent Conversation
Interruptible tools work the same way inside an agent (
genkit/exp): the agent streams the model's reply and pauses when a tool interrupts, and the client resumes over the same connection. Back a prompt agent with thetransfertool from above, drive a turn, and resume until it settles (a resumed turn can interrupt again):API Reference
Runtime Helpers (
ai/exp/tool-- experimental)The
toolpackage provides functions for use inside tool functions:Partial Tool Response Helpers (
aipackage)Tool Definitions (
ai/exp-- experimental)Define & Register
Create Without Registering
Genkit Convenience Functions (
genkit/exp-- experimental)The
genkit/exppackage (aliasedgenkitx) wraps the registry-level constructors above to register through a*genkit.Genkitinstead of a rawRegistry. The registry stays internal; these reach it through an internal bridge, so no public registry accessor is added. The same package also hosts the experimental agent constructors (DefineAgent,DefinePromptAgent,DefineCustomAgent).Function Signatures
InterruptibleTool[In, Out, Res]
Tool[In, Out]
Implements
ai.Tool-- usable anywhere a tool is accepted (ai.WithTools, etc.).