Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
161 commits
Select commit Hold shift + click to select a range
0c629f7
added `BidiAction` and `BidiFlow`
apascal07 Jan 31, 2026
f805988
added `NewBidiFlow`
apascal07 Jan 31, 2026
f0f5295
Update genkit.go
apascal07 Jan 31, 2026
9b9bdea
Update genkit.go
apascal07 Jan 31, 2026
1382035
added `SessionFlow` and related
apascal07 Feb 6, 2026
fe91d76
Update main.go
apascal07 Feb 6, 2026
ad323d2
Update main.go
apascal07 Feb 6, 2026
3913771
moved files
apascal07 Feb 6, 2026
15880f5
added `DefineSessionFlowFromPrompt`
apascal07 Feb 6, 2026
e94bca1
removed stream type param
apascal07 Feb 6, 2026
ec1aa79
Update action.go
apascal07 Feb 6, 2026
a77fa33
updates
apascal07 Feb 9, 2026
e83af30
cleaned up API naming and behavior
apascal07 Feb 17, 2026
db37102
Update action.go
apascal07 Feb 17, 2026
f4c4ec1
added stream capturing to output
apascal07 Feb 18, 2026
08b09e4
stream out interrupt chunks
apascal07 Feb 18, 2026
c2f55ab
Update genkit.go
apascal07 Feb 18, 2026
30b4afd
Update agent_flow.go
apascal07 Feb 18, 2026
22be814
removed get snapshot action
apascal07 Feb 18, 2026
a009a1b
tagged prompt messages and excluded them
apascal07 Feb 18, 2026
ea742d9
fixed PromptInput -> InputVariables
apascal07 Feb 18, 2026
2ae4bb8
added AgentFlowResult to output final artifacts
apascal07 Feb 18, 2026
787f61a
Update agent_flow.go
apascal07 Feb 18, 2026
d1281d5
removed turn index from snapshot
apascal07 Feb 18, 2026
d6cae44
exposed InputCh and TurnIndex on AgentSession
apascal07 Feb 18, 2026
10bbd03
improvements to API
apascal07 Feb 18, 2026
a687eb6
minor fixes
apascal07 Feb 18, 2026
7c535c4
Update session.go
apascal07 Feb 18, 2026
26f8f7d
added shared schemas for agent types
apascal07 Feb 18, 2026
d9c335a
Update typing.py
apascal07 Feb 18, 2026
fb7f254
various renames
apascal07 Feb 18, 2026
971b668
helper for input variables conversion
apascal07 Feb 19, 2026
3491761
DefinePromptAgent takes in prompt name instead of resolved prompt
apascal07 Feb 19, 2026
b46acce
fixed interrupts streaming
apascal07 Feb 21, 2026
6676882
Update genkit.go
apascal07 Feb 21, 2026
8d99639
added `SetMessages`
apascal07 Feb 21, 2026
3cbec28
added `AgentSession.Result()`
apascal07 Feb 25, 2026
bc87855
Merge branch 'main' into ap/go-bidi
apascal07 Feb 25, 2026
1f9cffc
Merge branch 'main' into ap/go-bidi
apascal07 Feb 25, 2026
be5564c
fixed types
apascal07 Feb 25, 2026
f943c96
fixed types
apascal07 Feb 25, 2026
fa04304
moved from `ai/x` to `ai/exp`
apascal07 Feb 25, 2026
ee4e68a
Update agent.go
apascal07 Feb 25, 2026
975accd
added `AgentFlow.Run()` and `AgentFlow.RunText()`
apascal07 Feb 25, 2026
a6f044b
dedupe consecutive identical snapshots
apascal07 Feb 25, 2026
f00786a
renamed agent flow et al to session flow
apascal07 Mar 6, 2026
3e707fc
Update genkit-schema.json
apascal07 Mar 6, 2026
5ff5520
renamed files
apascal07 Mar 6, 2026
66e5969
Update agent.ts
apascal07 Mar 6, 2026
9babe20
Update schemas.config
apascal07 Mar 6, 2026
06671f2
refactored type params
apascal07 Mar 13, 2026
2b13257
refactored type params
apascal07 Mar 13, 2026
ba7e5a4
Merge ap/go-bidi and refactor session flow for new type params
apascal07 Mar 13, 2026
ec9d05d
refactored order of type params
apascal07 Mar 13, 2026
cf00455
Update action.go
apascal07 Mar 13, 2026
00a2f88
Update action.go
apascal07 Mar 13, 2026
8867ea0
Update action.go
apascal07 Mar 13, 2026
e95a360
Update session_flow_test.go
apascal07 Mar 30, 2026
bf7b4ef
refactored EndTurn bool into TurnEnd struct
apascal07 Apr 17, 2026
9ef39c0
fix python
apascal07 Apr 17, 2026
51d9d30
regenerate genkit-schema.json
apascal07 Apr 17, 2026
7fd8f61
regenerate Python schema typing
apascal07 Apr 17, 2026
402d64e
Update session_flow.go
apascal07 Apr 21, 2026
b1d7cdd
Merge remote-tracking branch 'origin/main' into ap/go-bidi
apascal07 Apr 21, 2026
54f335d
Merge remote-tracking branch 'origin/main' into ap/go-bidi
apascal07 Apr 21, 2026
f7385f8
Merge branch 'ap/go-bidi' into ap/go-session-flow
apascal07 Apr 21, 2026
a5b97b0
feat(go): add background session flows via `Detach` (#5193)
apascal07 May 11, 2026
73e75c4
Merge remote-tracking branch 'origin/main' into ap/go-bidi
apascal07 May 11, 2026
b401b2a
Merge remote-tracking branch 'origin/main' into ap/go-bidi
apascal07 May 11, 2026
2840900
Merge branch 'ap/go-bidi' into ap/go-session-flow
apascal07 May 11, 2026
ab9de2e
refactor(go/exp): rename session flow to agent and unify definition p…
apascal07 May 11, 2026
29e64f4
refactor(go/exp): structured snapshot errors, ToolResume, status rena…
apascal07 May 12, 2026
13dbbe4
chore: appease gofmt and prettier
apascal07 May 12, 2026
fae13cb
chore(py): regenerate _typing.py for snapshot/resume schema changes
apascal07 May 12, 2026
fb948a4
fix(py): prefer the superset when two inline schemas share a name
apascal07 May 12, 2026
0cdb4d3
refactor(go/exp): tighten AgentInit validation, share SessionSnapshot…
apascal07 May 12, 2026
a02e991
fix(schema): make status optional on snapshot error to unblock Pydant…
apascal07 May 12, 2026
12e3990
refactor(go/exp): drop unused PromptIn type param from DefinePromptAgent
apascal07 May 12, 2026
081405d
refactor(go/exp): rename samples, add agent-inline, fix AgentDefineOp…
apascal07 May 12, 2026
9b290ea
test(go/exp): cover State mismatch for every typed AgentOption variant
apascal07 May 12, 2026
335c6df
refactor(go/exp): switch StateTransform to pointer signature
apascal07 May 12, 2026
56c8114
refactor(go/exp): unify DefineAgent with FromInline/FromPrompt sources
apascal07 May 12, 2026
43062f7
refactor(go/exp): single-message AgentInput, validate user role
apascal07 May 13, 2026
6cf858c
chore(py): regenerate _typing.py for AgentInput.message rename
apascal07 May 13, 2026
acdb8e6
fix(go/exp): ctx-aware Responder.Send to decouple fn liveness from sh…
apascal07 May 13, 2026
3888066
refactor(go/core,go/exp): break-out of BidiConnection.Receive no long…
apascal07 May 13, 2026
934e36d
chore(go/samples/agent-prompt): reference ChatPromptInput by name
apascal07 May 13, 2026
50ebd0f
chore(go/samples/agent-inline): use gemini-flash-latest
apascal07 May 13, 2026
3e29774
fix(go/exp): deep-copy Result and AgentOutput to isolate from session
apascal07 May 13, 2026
669c723
feat(go/exp/localstore): file-based session store; fix sample Ctrl+C
apascal07 May 13, 2026
66f4eb8
refactor(go/exp): AgentConnection.Output() implicitly closes and drains
apascal07 May 14, 2026
53a9878
refactor(go): rename ActionDesc schema fields and lowercase json tags
apascal07 May 14, 2026
678bae9
refactor(go): rename ActionDesc schema fields and lowercase json tags
apascal07 May 14, 2026
967ff79
Merge branch 'ap/go-bidi' into ap/go-session-flow
apascal07 May 14, 2026
a646022
feat(go): unified basic-agents sample; add LatestSnapshot and Agent.N…
apascal07 May 14, 2026
e760e94
fix(go/exp/localstore): never drop the latest snapshot status notific…
apascal07 Jun 9, 2026
b77a81b
feat(go/exp): agent finish reasons on TurnEnd, AgentOutput, and snaps…
apascal07 Jun 9, 2026
166a4da
feat(go/core): errors.Is sentinels for BidiConnection.Send failures
apascal07 Jun 9, 2026
4a13960
feat(go/exp): resolve agent failures gracefully with last-good state
apascal07 Jun 9, 2026
fbdf310
Merge remote-tracking branch 'origin/ap/go-bidi' into ap/go-session-flow
apascal07 Jun 10, 2026
67ae704
fix(go/core): camelCase json tags for ActionDesc schema fields
apascal07 Jun 10, 2026
059d9e4
feat(go/exp): session IDs and resume-by-session via WithSessionID
apascal07 Jun 10, 2026
6ee6b22
Merge remote-tracking branch 'origin/main' into ap/go-bidi
apascal07 Jun 10, 2026
ee966ca
Merge branch 'ap/go-bidi' into ap/go-session-flow
apascal07 Jun 10, 2026
165293c
fix(go/exp): prevent agent process crashes from shared prompt metadat…
apascal07 Jun 10, 2026
670ccc2
fix(go/core): release bidi connection contexts and unwedge abandoned …
apascal07 Jun 11, 2026
d20cfe4
fix(go/exp): isolate session state and apply stream side effects sync…
apascal07 Jun 11, 2026
011c38f
feat(go): rework bidi actions around a typed BidiConnection and JSON …
apascal07 Jun 12, 2026
fb63565
fix(go): reject init for non-bidi actions before committing to SSE
apascal07 Jun 12, 2026
ea4801c
fix(tools): add init to Zod source schemas and regenerate Python typings
apascal07 Jun 12, 2026
1d011e8
chore(ci): check schema freshness and all jsonschemagen outputs
apascal07 Jun 12, 2026
ea80971
Merge branch 'ap/go-bidi' into ap/go-session-flow
apascal07 Jun 12, 2026
5b0e9e8
refactor(core): single-source the Genkit error wire shape as RuntimeE…
apascal07 Jun 12, 2026
9a3eee6
fix(go/core): run no-init bidi actions and keep stack traces off the …
apascal07 Jun 12, 2026
efe47c0
feat(go/samples): add basic-agents-server sample serving agents over …
apascal07 Jun 12, 2026
28d59a6
fix(go/genkit): recover handler panics in the V2 reflection server
apascal07 Jun 12, 2026
b487782
feat(go/exp): register agents under the agent action type
apascal07 Jun 12, 2026
290d136
fix(go/core): reject absent input on one-shot bidi runs with a clear …
apascal07 Jun 12, 2026
c1fecdc
feat(go/exp): serve agents over HTTP and consolidate experimental hel…
apascal07 Jun 16, 2026
78648c9
feat(go/exp): stream custom state as JSON Patch deltas
apascal07 Jun 16, 2026
417f6d0
feat(go/exp): rework agent session and snapshot flow
apascal07 Jun 16, 2026
282826f
fix(py): escape reserved words in the schema typing generator
apascal07 Jun 16, 2026
a072bdc
refactor(go/exp): snapshot at turn end only
apascal07 Jun 17, 2026
b5fc333
refactor(go/exp): drop redundant snapshotId from AbortSnapshotResponse
apascal07 Jun 17, 2026
c5741b6
chore(py): regenerate schema typing for agent schema changes
apascal07 Jun 17, 2026
0aea679
refactor(go/exp): replace Mount helper with inline route loop
apascal07 Jun 17, 2026
44098de
chore(go/ai): drop per-call experimental-model log
apascal07 Jun 17, 2026
8bc383d
feat(go/exp): validate agent resume parts against session history
apascal07 Jun 17, 2026
46ab093
test(go/exp): streamline agent test suite with shared helpers
apascal07 Jun 17, 2026
21924e9
refactor(go/exp): unexport SessionRunner.inputCh and turnIndex
apascal07 Jun 17, 2026
9a0d09b
fix(go/ai): emit parallel tool responses in request order
apascal07 Jun 17, 2026
b63375a
docs(go/exp): tighten godocs for the agent API
apascal07 Jun 18, 2026
6a13883
Merge branch 'main' into ap/go-bidi
apascal07 Jun 18, 2026
3bf24c7
Merge branch 'ap/go-bidi' into ap/go-session-flow
apascal07 Jun 18, 2026
e2341f3
refactor(go): drop core/x/session for the ai/exp session
apascal07 Jun 18, 2026
bda6531
feat(go/exp): detect orphaned detached turns via snapshot heartbeat
apascal07 Jun 19, 2026
2a48ce2
refactor(go/core): rename bidi connection methods for clarity
apascal07 Jun 19, 2026
2b333dc
Merge branch 'ap/go-bidi' into ap/go-session-flow
apascal07 Jun 19, 2026
6a59525
refactor(go/exp): rename agent StreamBidi to Connect
apascal07 Jun 19, 2026
79608f4
feat(go/exp): add agent snapshot facade for transform-applied reads
apascal07 Jun 20, 2026
0e0f41b
fix(go/core): validate and normalize bidi JSON init like action input
apascal07 Jun 22, 2026
1e9654c
feat(go/exp): tag agent traces with session ID, match JS turn spans
apascal07 Jun 22, 2026
0bd7227
feat(go/exp): add pruning and path-prefix options to file session store
apascal07 Jun 23, 2026
e5707bf
feat(go/exp): observe cross-process snapshot status changes in file s…
apascal07 Jun 23, 2026
4c1e89e
Update README.md
apascal07 Jun 23, 2026
9268759
feat(go/exp): add session-flow middleware package and context seeding
apascal07 Jun 23, 2026
ad7941b
Merge branch 'ap/go-session-flow' into ap/go-session-flow-middleware
apascal07 Jun 23, 2026
4e79302
feat(go/ai/exp): rework agent sources into InlinePrompt/SameNamedProm…
apascal07 Jun 23, 2026
c15b1a6
feat(go/ai/exp): make DefineAgent inline-only, add DefinePromptAgent
apascal07 Jun 23, 2026
ad5d0cd
docs(go): match agents README to the DefineAgent/DefinePromptAgent split
apascal07 Jun 23, 2026
06e2b5a
feat(go/ai/exp): record session state and snapshot ID on agent turn s…
apascal07 Jun 23, 2026
82c3621
feat(go/ai/exp): add WithStreamTransform; transforms fail closed on e…
apascal07 Jun 24, 2026
0abd6f5
feat(go/ai/exp): return snapshotId on AbortSnapshotResponse
apascal07 Jun 24, 2026
1d7e550
Merge branch 'ap/go-session-flow' into ap/go-session-flow-middleware
apascal07 Jun 24, 2026
4412982
fix(go/plugins/middleware/exp): address PR review feedback
apascal07 Jun 25, 2026
d5150f9
Merge remote-tracking branch 'origin/main' into ap/go-session-flow-mi…
apascal07 Jun 25, 2026
ec6cf16
chore(go): move Go sample ignores out of the root .gitignore
apascal07 Jun 25, 2026
d8b3a71
fix(go/ai/exp): expose the typed output schema on exp tools
apascal07 Jun 25, 2026
cd34ade
refactor(go/plugins/middleware/exp): build tools with the ai/exp tool…
apascal07 Jun 25, 2026
898a928
refactor(go/ai/exp): seed the genkit instance internally, drop public…
apascal07 Jun 25, 2026
9102800
docs(go): document the experimental sub-agent delegation middleware
apascal07 Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ js/testapps/firebase-functions-sample1/public/config.js
.genkit
js/**/.genkit
samples/**/.genkit
go/**/.genkit
# Compiled Go sample binaries (e.g. `go build ./samples/<name>` writes go/<name>)
/go/custom-agent
/go/x-agent-interrupts
ui-debug.log
firebase-debug.log
firestore-debug.log
Expand Down
4 changes: 4 additions & 0 deletions go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Compiled sample binaries produced by `go build ./samples/<name>` from this
# directory (e.g. ./samples/basic-agents). They are build artifacts, not source.
/basic-agents
/basic-agents-server
53 changes: 53 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,59 @@ Detach requires a store that implements `SnapshotSubscriber` (both bundled local

[See full example](samples/basic-agents)

### Delegate to Sub-Agents

The experimental `Agents` middleware (in `plugins/middleware/exp`) lets one agent delegate to others. It injects one `delegate_to_<name>` tool per sub-agent and a `<sub-agents>` listing into the orchestrator's system prompt, then runs the chosen sub-agent and returns its result when the model calls the tool. Each sub-agent's `aix.WithDescription` (captured by `agent.Ref()`) tells the orchestrator when to reach for it:

```go
import (
"github.com/firebase/genkit/go/ai"
aix "github.com/firebase/genkit/go/ai/exp"
"github.com/firebase/genkit/go/ai/exp/localstore"
genkitx "github.com/firebase/genkit/go/genkit/exp"
middlewarex "github.com/firebase/genkit/go/plugins/middleware/exp"
)

researcher := genkitx.DefineAgent(g, "researcher",
aix.InlinePrompt{
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("You are a thorough research assistant. Summarize well-sourced findings."),
},
aix.WithDescription[any]("Researches a topic and summarizes well-sourced findings."),
)

engineer := genkitx.DefineAgent(g, "engineer",
aix.InlinePrompt{
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("You are an expert programmer. Write clean, well-commented code."),
},
aix.WithDescription[any]("Writes and explains code."),
)

// The orchestrator delegates instead of answering directly: the model calls
// delegate_to_researcher / delegate_to_engineer and the middleware runs them.
orchestrator := genkitx.DefineAgent(g, "orchestrator",
aix.InlinePrompt{
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("You are a project coordinator. Delegate to the right sub-agent, " +
"then synthesize a final answer."),
ai.WithUse(&middlewarex.Agents{
Agents: []aix.AgentRef{researcher.Ref(), engineer.Ref()},
MaxDelegations: 5, // cap delegation tool calls per turn (0 = unlimited)
HistoryLength: 4, // recent messages forwarded to client-managed sub-agents
}),
},
aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()),
)

out, _ := orchestrator.RunText(ctx, "Research goroutine scheduling, then sketch a worker pool.")
fmt.Println(out.Message.Text())
```

Sub-agents are named by `aix.AgentRef`, either captured from an agent value with `agent.Ref()` or written by hand (`aix.AgentRef{Name: "researcher"}`). The middleware composes with the `Artifacts` middleware: give the sub-agents `&middlewarex.Artifacts{}` so they can save output, set `ArtifactStrategy: middlewarex.ArtifactStrategySession` to merge those artifacts into the orchestrator's session instead of inlining them in the tool result, and add `&middlewarex.Artifacts{Readonly: true}` on the orchestrator so it can review them before answering.

[See full example](samples/basic-agents)

### Serve Agents over HTTP

An `Agent` is an `api.BidiAction`, so it serves over HTTP one turn per request. The `genkit/exp` package lays out a default route surface for every registered agent, including the snapshot companion endpoints for store-backed agents:
Expand Down
33 changes: 32 additions & 1 deletion go/ai/exp/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"github.com/firebase/genkit/go/core/logger"
"github.com/firebase/genkit/go/core/tracing"
"github.com/firebase/genkit/go/internal/base"
"github.com/firebase/genkit/go/internal/genkitbridge"
"github.com/google/uuid"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
Expand Down Expand Up @@ -727,6 +728,24 @@ func DefineAgent[State any](
return DefineCustomAgent(r, name, agentLoop[State](r, p, nil), opts...)
}

// genkitContextSeed returns a context decorator that seeds the host Genkit
// instance into each agent invocation, so the agent's prompt, tools, and
// middleware can retrieve it via genkit.FromContext and resolve or run other
// actions. The instance is reconstructed from r by the genkit package through
// the internal bridge, so the registry-level constructors below wire seeding up
// themselves and there is no public option for it.
//
// It returns nil when the genkit package is not linked into the build, leaving
// an agent defined directly on a bare registry untouched.
func genkitContextSeed(r api.Registry) func(context.Context) context.Context {
if genkitbridge.SeedContextForRegistry == nil {
return nil
}
return func(ctx context.Context) context.Context {
return genkitbridge.SeedContextForRegistry(ctx, r)
}
}

// DefinePromptAgent defines a prompt-backed agent and registers it, sourcing
// its prompt from the registry by name. Each turn renders the prompt, appends
// conversation history, calls the model with streaming, and updates session
Expand Down Expand Up @@ -754,6 +773,9 @@ func DefinePromptAgent[State any](
name string,
opts ...PromptAgentOption[State],
) *Agent[State] {
if seed := genkitContextSeed(r); seed != nil {
opts = append(opts, &agentOptions[State]{contextFunc: seed})
}
cfg := &promptAgentOptions[State]{}
for _, opt := range opts {
if err := opt.applyPromptAgent(cfg); err != nil {
Expand Down Expand Up @@ -818,7 +840,7 @@ func newCustomAgent[State any](
cfg *agentOptions[State],
) *Agent[State] {
// Typed under ActionTypeAgent so agents surface as their own action
// kind rather than as flows (genkit.ListAgents vs ListFlows). Built on
// kind rather than as flows (genkit/exp.ListAgents vs genkit.ListFlows). Built on
// NewBidiAction so the agent capability metadata is set at construction
// time; actions must be immutable once registered. WithFlowContext
// below preserves the flow-context wrapping that makes core.Run work
Expand All @@ -841,6 +863,12 @@ func newCustomAgent[State any](
outCh chan<- *AgentStreamChunk,
) (*AgentOutput[State], error) {
ctx = core.WithFlowContext(ctx, name)
// Apply any context decorators (e.g. the genkit package seeding its
// instance) before the runtime derives the per-turn work context, so
// the decorated values reach each turn's prompt, tools, and middleware.
if cfg.contextFunc != nil {
ctx = cfg.contextFunc(ctx)
}
rt, err := newAgentRuntime(ctx, name, cfg, in, inCh, outCh)
if err != nil {
// Init failures (a rejected init payload, a failed
Expand Down Expand Up @@ -880,6 +908,9 @@ func DefineCustomAgent[State any](
fn AgentFunc[State],
opts ...AgentOption[State],
) *Agent[State] {
if seed := genkitContextSeed(r); seed != nil {
opts = append(opts, &agentOptions[State]{contextFunc: seed})
}
a := NewCustomAgent(name, fn, opts...)
a.Register(r)
return a
Expand Down
11 changes: 11 additions & 0 deletions go/ai/exp/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ type agentOptions[State any] struct {
transform StateTransform[State]
streamTransform StreamTransform
description string
// contextFunc decorates each invocation's context once before the turn
// loop runs. It has no public option: the registry-level constructors set
// it internally to seed the genkit instance (see genkitContextSeed in
// agent.go), so callers reach the instance via genkit.FromContext.
contextFunc func(context.Context) context.Context
}

func (o *agentOptions[State]) applyAgent(opts *agentOptions[State]) error {
Expand Down Expand Up @@ -138,6 +143,12 @@ func (o *agentOptions[State]) applyAgent(opts *agentOptions[State]) error {
}
opts.description = o.description
}
if o.contextFunc != nil {
// Seeded internally by the registry-level constructors
// (genkitContextSeed), at most once per agent, so this is a plain set
// rather than a compose of multiple decorators.
opts.contextFunc = o.contextFunc
}
return nil
}

Expand Down
49 changes: 49 additions & 0 deletions go/ai/exp/ref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package exp

// AgentRef refers to an agent by name, optionally carrying a description. It is
// the agent analog of [ai.ModelRef] / [ai.ToolRef]: a small, JSON-serializable
// value that names an agent for resolution against a registry. Like those, it
// resolves by name (the path the Dev UI, HTTP serving, and ListAgents all use),
// so the referenced agent must be registered wherever the ref is consumed.
//
// Build one by name with a struct literal, or derive it from an agent value
// with [Agent.Ref], which fills in the name and description so callers need not
// restate either:
//
// aix.AgentRef{Name: "researcher"}
// coderAgent.Ref()
type AgentRef struct {
// Name identifies the agent, resolved as /agent/<Name>. Required.
Name string `json:"name"`
// Description is a human-readable description used by consumers that list
// agents (e.g. the agents middleware's system prompt). [Agent.Ref] fills it
// from the agent's descriptor. Optional.
Description string `json:"description,omitempty"`
}

// Ref returns an [AgentRef] for this agent, capturing its name and description
// so callers can reference it without restating either, and without a name
// string that can drift from the agent. Resolution remains by name, so the
// agent must be registered (as [DefineAgent] does) wherever the ref is used.
func (a *Agent[State]) Ref() AgentRef {
return AgentRef{
Name: a.Name(),
Description: a.Desc().Description,
}
}
23 changes: 23 additions & 0 deletions go/ai/exp/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,3 +510,26 @@ func SessionFromContext[State any](ctx context.Context) *Session[State] {
session, _ := sessionCtxKey.FromContext(ctx).(*Session[State])
return session
}

// ArtifactStore is the State-agnostic view of a session's artifact collection.
// Every [Session] satisfies it regardless of its State type, since artifact
// operations do not touch custom state. Middleware and tools that work with
// artifacts without knowing the agent's State type use it via
// [ArtifactStoreFromContext], where [SessionFromContext] cannot help because it
// requires the concrete State.
type ArtifactStore interface {
// Artifacts returns a snapshot of the session's current artifacts.
Artifacts() []*Artifact
// AddArtifacts adds artifacts, replacing any existing artifact of the same
// name.
AddArtifacts(artifacts ...*Artifact)
}

// ArtifactStoreFromContext returns the active session's artifacts as a
// State-agnostic [ArtifactStore], or nil if there is no active session in ctx.
// Unlike [SessionFromContext] it does not require knowing the session's State
// type, so it is the accessor for middleware and tools.
func ArtifactStoreFromContext(ctx context.Context) ArtifactStore {
store, _ := sessionCtxKey.FromContext(ctx).(ArtifactStore)
return store
}
37 changes: 33 additions & 4 deletions go/ai/exp/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import (
"context"
"errors"
"fmt"
"reflect"

"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/ai/exp/tool"
"github.com/firebase/genkit/go/core"
"github.com/firebase/genkit/go/core/api"
"github.com/firebase/genkit/go/internal/base"
)
Expand All @@ -45,15 +47,42 @@ type Tool[In, Out any] struct {
inner *ai.ToolDef[In, *ai.MultipartToolResponse] // DEPRECATED(breaking): remove wrapper; Tool owns the action directly.
}

// DEPRECATED(breaking): All methods below exist only to implement ai.Tool by
// delegating to the wrapped ai.ToolDef. With breaking changes, Tool would own
// the action directly and implement these natively without delegation.
// DEPRECATED(breaking): The methods below exist to implement ai.Tool on top of
// the wrapped ai.ToolDef. Most are pure delegation; Definition additionally
// restores the real output schema (see its comment). With breaking changes, Tool
// would own the action directly and implement these natively, inferring the
// output schema from Out without the override.

// Name returns the name of the tool.
func (t *Tool[In, Out]) Name() string { return t.inner.Name() }

// Definition returns the [ai.ToolDefinition] for this tool.
func (t *Tool[In, Out]) Definition() *ai.ToolDefinition { return t.inner.Definition() }
//
// The inner tool is built on [ai.NewMultipartTool], whose function returns
// *[ai.MultipartToolResponse], so the inner definition would advertise that
// envelope as the output schema. We override OutputSchema with the schema
// inferred from the Out type parameter, making the definition equivalent to what
// [ai.NewTool] exposes (the real output type) rather than leaking the multipart
// envelope to the model and Dev UI. Genkit infers schemas with DoNotReference,
// so the result is fully inlined and needs no registry resolution.
func (t *Tool[In, Out]) Definition() *ai.ToolDefinition {
def := t.inner.Definition()
if schema := inferOutputSchema[Out](); schema != nil {
def.OutputSchema = schema
}
return def
}

// inferOutputSchema returns the inlined JSON schema for the Out type parameter,
// or nil when Out carries no schema (e.g. any), mirroring how [ai.NewTool]
// derives its output schema from the output type.
func inferOutputSchema[Out any]() map[string]any {
var zero Out
if reflect.TypeOf(zero) == nil {
return nil
}
return core.InferSchemaMap(zero)
}

// RunRaw runs the tool with raw input.
func (t *Tool[In, Out]) RunRaw(ctx context.Context, input any) (any, error) {
Expand Down
52 changes: 52 additions & 0 deletions go/ai/exp/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package exp

import (
"context"
"reflect"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -118,6 +119,57 @@ func TestTool_AttachParts(t *testing.T) {
}
}

type reportItem struct {
Name string `json:"name"`
}

type reportOut struct {
Title string `json:"title"`
Items []reportItem `json:"items"`
}

// TestTool_OutputSchemaMatchesClassic guards against the multipart envelope
// leaking into the tool definition. aix.NewTool wraps ai.NewMultipartTool, whose
// function returns *MultipartToolResponse; without Definition restoring the real
// output schema, the model and Dev UI would see the envelope ({content, output,
// metadata}) instead of the actual Out type. The exp tool must advertise the
// same output schema ai.NewTool would, including for the interruptible variant
// (which embeds Tool) and for a nested struct that exercises schema inlining.
func TestTool_OutputSchemaMatchesClassic(t *testing.T) {
classic := ai.NewTool("classic", "d",
func(tc *ai.ToolContext, _ weatherIn) (reportOut, error) { return reportOut{}, nil })
want := classic.Definition().OutputSchema
if want == nil {
t.Fatal("ai.NewTool unexpectedly produced a nil output schema")
}

simple := NewTool("exp-simple", "d",
func(ctx context.Context, _ weatherIn) (reportOut, error) { return reportOut{}, nil })
interruptible := NewInterruptibleTool("exp-interruptible", "d",
func(ctx context.Context, _ weatherIn, _ *confirmation) (reportOut, error) { return reportOut{}, nil })

for _, tc := range []struct {
name string
got any
}{
{"NewTool", simple.Definition().OutputSchema},
{"NewInterruptibleTool", interruptible.Definition().OutputSchema},
} {
if !reflect.DeepEqual(tc.got, want) {
t.Errorf("%s output schema = %#v\nwant %#v (matching ai.NewTool)", tc.name, tc.got, want)
}
// Explicit guard on intent: the real Out fields are present and the
// MultipartToolResponse envelope fields are not.
props, _ := tc.got.(map[string]any)["properties"].(map[string]any)
if _, ok := props["title"]; !ok {
t.Errorf("%s output schema missing the real %q field: %#v", tc.name, "title", tc.got)
}
if _, ok := props["content"]; ok {
t.Errorf("%s output schema leaked the multipart envelope (has %q): %#v", tc.name, "content", tc.got)
}
}
}

// TestTool_SendPartialNoOpWithoutStreaming confirms SendPartial is a safe no-op
// when no streaming callback is wired (here, a direct RunRaw).
func TestTool_SendPartialNoOpWithoutStreaming(t *testing.T) {
Expand Down
Loading
Loading