Skip to content
Closed
35 changes: 31 additions & 4 deletions internal/toolinternal/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package toolinternal

import (
"context"
"errors"

"github.com/google/uuid"
"google.golang.org/genai"
Expand All @@ -28,12 +29,18 @@ import (
"google.golang.org/adk/tool"
)

// ErrArtifactServiceNotConfigured is returned when artifact service operations are attempted without configuration.
var ErrArtifactServiceNotConfigured = errors.New("artifact service not configured")

type internalArtifacts struct {
agent.Artifacts
eventActions *session.EventActions
}

func (ia *internalArtifacts) Save(ctx context.Context, name string, data *genai.Part) (*artifact.SaveResponse, error) {
if ia == nil {
return nil, ErrArtifactServiceNotConfigured
}
resp, err := ia.Artifacts.Save(ctx, name, data)
if err != nil {
return resp, err
Expand All @@ -48,6 +55,20 @@ func (ia *internalArtifacts) Save(ctx context.Context, name string, data *genai.
return resp, nil
}

func (ia *internalArtifacts) List(ctx context.Context) (*artifact.ListResponse, error) {
if ia == nil {
return nil, ErrArtifactServiceNotConfigured
}
return ia.Artifacts.List(ctx)
}

func (ia *internalArtifacts) Load(ctx context.Context, name string) (*artifact.LoadResponse, error) {
if ia == nil {
return nil, ErrArtifactServiceNotConfigured
}
return ia.Artifacts.Load(ctx, name)
}

func NewToolContext(ctx agent.InvocationContext, functionCallID string, actions *session.EventActions) tool.Context {
if functionCallID == "" {
functionCallID = uuid.NewString()
Expand All @@ -60,15 +81,21 @@ func NewToolContext(ctx agent.InvocationContext, functionCallID string, actions
}
cbCtx := contextinternal.NewCallbackContextWithDelta(ctx, actions.StateDelta)

// Only create internalArtifacts if the underlying Artifacts service is configured
var artifacts *internalArtifacts
if ctx.Artifacts() != nil {
artifacts = &internalArtifacts{
Artifacts: ctx.Artifacts(),
eventActions: actions,
}
}

return &toolContext{
CallbackContext: cbCtx,
invocationContext: ctx,
functionCallID: functionCallID,
eventActions: actions,
artifacts: &internalArtifacts{
Artifacts: ctx.Artifacts(),
eventActions: actions,
},
artifacts: artifacts,
}
}

Expand Down
43 changes: 43 additions & 0 deletions internal/toolinternal/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package toolinternal

import (
"errors"
"testing"

"google.golang.org/adk/agent"
Expand All @@ -36,3 +37,45 @@ func TestToolContext(t *testing.T) {
t.Errorf("ToolContext(%+T) is unexpectedly an InvocationContext", got)
}
}

func TestInternalArtifacts_NilSafe(t *testing.T) {
// Create invocation context without artifact service
inv := contextinternal.NewInvocationContext(t.Context(), contextinternal.InvocationContextParams{
Artifacts: nil,
})
toolCtx := NewToolContext(inv, "fn1", &session.EventActions{})

artifacts := toolCtx.Artifacts()
// artifacts will be nil when service not configured

tests := []struct {
name string
call func() (any, error)
}{
{
name: "List",
call: func() (any, error) { return artifacts.List(t.Context()) },
},
{
name: "Load",
call: func() (any, error) { return artifacts.Load(t.Context(), "test.txt") },
},
{
name: "Save",
call: func() (any, error) { return artifacts.Save(t.Context(), "test.txt", nil) },
},
}

for _, tt := range tests {
t.Run(tt.name+" returns error", func(t *testing.T) {
_, err := tt.call()
if err == nil {
t.Error("Expected an error, got nil")
return
}
if !errors.Is(err, ErrArtifactServiceNotConfigured) {
t.Errorf("Expected ErrArtifactServiceNotConfigured, got: %v", err)
}
})
}
}
36 changes: 36 additions & 0 deletions runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ func New(cfg Config) (*Runner, error) {
return nil, fmt.Errorf("failed to create agent tree: %w", err)
}

// Validate that required services are configured for tools
if err := validateConfiguration(cfg.Agent, cfg.ArtifactService); err != nil {
return nil, err
}

return &Runner{
appName: cfg.AppName,
rootAgent: cfg.Agent,
Expand Down Expand Up @@ -268,3 +273,34 @@ func findAgent(curAgent agent.Agent, targetName string) agent.Agent {
}
return nil
}

// validateConfiguration checks that required services are available for tools.
func validateConfiguration(rootAgent agent.Agent, artifactService artifact.Service) error {
return walkAgentTree(rootAgent, func(a agent.Agent) error {
Comment thread
ShammiAnand marked this conversation as resolved.
Outdated
llmAgent, ok := a.(llminternal.Agent)
if !ok {
return nil
}

state := llminternal.Reveal(llmAgent)
for _, t := range state.Tools {
if t.Name() == "load_artifacts" && artifactService == nil {
return fmt.Errorf("agent %q uses load_artifacts tool but ArtifactService not configured in runner", a.Name())
}
}
return nil
})
}

// walkAgentTree recursively walks the agent tree and applies fn to each agent.
func walkAgentTree(a agent.Agent, fn func(agent.Agent) error) error {
if err := fn(a); err != nil {
return err
}
for _, sub := range a.SubAgents() {
if err := walkAgentTree(sub, fn); err != nil {
return err
}
}
return nil
}
85 changes: 83 additions & 2 deletions runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ import (
"strings"
"testing"

"google.golang.org/genai"

"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/artifact"
"google.golang.org/adk/session"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/loadartifactstool"
"google.golang.org/genai"
)

func TestRunner_findAgentToRun(t *testing.T) {
Expand Down Expand Up @@ -314,6 +315,86 @@ func TestRunner_SaveInputBlobsAsArtifacts(t *testing.T) {
}
}

func TestNew_ValidatesLoadArtifactsToolRequiresArtifactService(t *testing.T) {
t.Parallel()

tests := []struct {
name string
agent agent.Agent
artifactService artifact.Service
wantErr bool
errContains string
}{
{
name: "error when load_artifacts tool present but no artifact service",
agent: must(llmagent.New(llmagent.Config{
Name: "test_agent",
Tools: []tool.Tool{loadartifactstool.New()},
})),
artifactService: nil,
wantErr: true,
errContains: "load_artifacts tool but ArtifactService not configured",
},
{
name: "ok when load_artifacts tool and artifact service both present",
agent: must(llmagent.New(llmagent.Config{
Name: "test_agent",
Tools: []tool.Tool{loadartifactstool.New()},
})),
artifactService: artifact.InMemoryService(),
wantErr: false,
},
{
name: "ok when no load_artifacts tool and no artifact service",
agent: must(llmagent.New(llmagent.Config{
Name: "test_agent",
})),
artifactService: nil,
wantErr: false,
},
{
name: "error when load_artifacts in sub-agent but no artifact service",
agent: must(llmagent.New(llmagent.Config{
Name: "parent_agent",
SubAgents: []agent.Agent{
must(llmagent.New(llmagent.Config{
Name: "child_agent",
Tools: []tool.Tool{loadartifactstool.New()},
})),
},
})),
artifactService: nil,
wantErr: true,
errContains: "child_agent",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := New(Config{
AppName: "testApp",
Agent: tt.agent,
SessionService: session.InMemoryService(),
ArtifactService: tt.artifactService,
})

if tt.wantErr {
if err == nil {
t.Errorf("New() expected error but got nil")
return
}
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("New() error = %v, want error containing %q", err, tt.errContains)
}
} else {
if err != nil {
t.Errorf("New() unexpected error = %v", err)
}
}
})
}
}

// creates agentTree for tests and returns references to the agents
func agentTree(t *testing.T) agentTreeStruct {
t.Helper()
Expand Down
27 changes: 27 additions & 0 deletions tool/loadartifactstool/load_artifacts_tool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package loadartifactstool_test

import (
"errors"
"strings"
"testing"

Expand Down Expand Up @@ -277,6 +278,32 @@ func TestLoadArtifactsTool_ProcessRequest_Artifacts_OtherFunctionCall(t *testing
}
}

func TestLoadArtifactsTool_ProcessRequest_NoArtifactService(t *testing.T) {
loadArtifactsTool := loadartifactstool.New()

// Create tool context WITHOUT artifact service configured
ctx := icontext.NewInvocationContext(t.Context(), icontext.InvocationContextParams{
Artifacts: nil, // No artifact service
})
tc := toolinternal.NewToolContext(ctx, "", nil)

llmRequest := &model.LLMRequest{}

requestProcessor, ok := loadArtifactsTool.(toolinternal.RequestProcessor)
if !ok {
t.Fatal("loadArtifactsTool does not implement RequestProcessor")
}

err := requestProcessor.ProcessRequest(tc, llmRequest)
if err == nil {
t.Fatal("Expected error when artifact service not configured, got nil")
}

if !errors.Is(err, toolinternal.ErrArtifactServiceNotConfigured) {
t.Errorf("Expected ErrArtifactServiceNotConfigured, got: %v", err)
}
}

func createToolContext(t *testing.T) tool.Context {
t.Helper()

Expand Down