Skip to content

Commit 50662f7

Browse files
authored
feat: don't use context for executor (#622)
1 parent 1e7b9c1 commit 50662f7

4 files changed

Lines changed: 105 additions & 13 deletions

File tree

pkg/agent/conversation.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,10 @@ func (s *Agent) Init(ctx context.Context) error {
277277
s.workDir = workDir
278278

279279
// Register tools with executor if none registered yet
280-
// We need to preserve existing tools (e.g. custom tools) while ensuring we have a fresh map
281-
// for this agent instance to avoid polluting the global default tools.
282-
existingTools := s.Tools.AllTools()
283-
s.Tools.Init()
284-
for _, tool := range existingTools {
285-
s.Tools.RegisterTool(tool)
286-
}
280+
// We clone existing tools (e.g. custom tools) to ensure we have a fresh map
281+
// This avoids polluting the global default tools and ensures thread safety.
282+
s.Tools = s.Tools.CloneWithExecutor(s.executor)
283+
287284
s.Tools.RegisterTool(tools.NewBashTool(s.executor))
288285
s.Tools.RegisterTool(tools.NewKubectlTool(s.executor))
289286

@@ -913,6 +910,9 @@ func (c *Agent) NewSession() (string, error) {
913910
c.executor = sb
914911
klog.Info("Created new sandbox for new session", "name", sandboxName)
915912

913+
// Re-bind all tools to the new executor
914+
c.Tools = c.Tools.CloneWithExecutor(c.executor)
915+
916916
c.Tools.RegisterTool(tools.NewBashTool(c.executor))
917917
c.Tools.RegisterTool(tools.NewKubectlTool(c.executor))
918918
c.sessionMu.Unlock()

pkg/tools/custom_tool.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ type CustomToolConfig struct {
3636

3737
// CustomTool implements the Tool interface for external commands.
3838
type CustomTool struct {
39-
config CustomToolConfig
39+
config CustomToolConfig
40+
executor sandbox.Executor
4041
}
4142

4243
// NewCustomTool creates a new CustomTool instance.
@@ -139,11 +140,9 @@ func (t *CustomTool) Run(ctx context.Context, args map[string]any) (any, error)
139140
workDir := ctx.Value(WorkDirKey).(string)
140141
env := os.Environ()
141142

142-
// Get executor from context or default to local
143-
var executor sandbox.Executor
144-
if v := ctx.Value(ExecutorKey); v != nil {
145-
executor = v.(sandbox.Executor)
146-
} else {
143+
// Use the injected executor, or fallback to local if not set (e.g. for global instance)
144+
executor := t.executor
145+
if executor == nil {
147146
executor = sandbox.NewLocalExecutor()
148147
}
149148

@@ -159,3 +158,12 @@ func (t *CustomTool) CheckModifiesResource(args map[string]any) string {
159158
// For custom tools, we'll conservatively use "unknown" since we can't
160159
return "unknown"
161160
}
161+
162+
// CloneWithExecutor creates a copy of the CustomTool with the given executor.
163+
// This is used to create a session-specific instance of the tool.
164+
func (t *CustomTool) CloneWithExecutor(executor sandbox.Executor) *CustomTool {
165+
return &CustomTool{
166+
config: t.config,
167+
executor: executor,
168+
}
169+
}

pkg/tools/custom_tool_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
package tools
1616

1717
import (
18+
"context"
19+
"strings"
1820
"testing"
21+
22+
"github.com/GoogleCloudPlatform/kubectl-ai/pkg/sandbox"
1923
)
2024

2125
func TestCustomTool_AddCommandPrefix(t *testing.T) {
@@ -111,3 +115,64 @@ func TestCustomTool_AddCommandPrefix(t *testing.T) {
111115
})
112116
}
113117
}
118+
119+
// MockExecutor implements sandbox.Executor for testing
120+
type MockExecutor struct {
121+
CapturedCommand string
122+
CapturedEnv []string
123+
CapturedWorkDir string
124+
}
125+
126+
func (m *MockExecutor) Execute(ctx context.Context, command string, env []string, workDir string) (*sandbox.ExecResult, error) {
127+
m.CapturedCommand = command
128+
m.CapturedEnv = env
129+
m.CapturedWorkDir = workDir
130+
return &sandbox.ExecResult{Stdout: "executed"}, nil
131+
}
132+
133+
func (m *MockExecutor) Close(ctx context.Context) error {
134+
return nil
135+
}
136+
137+
func TestCustomTool_CloneWithExecutor(t *testing.T) {
138+
config := CustomToolConfig{
139+
Name: "test-tool",
140+
Command: "echo",
141+
}
142+
143+
tool, err := NewCustomTool(config)
144+
if err != nil {
145+
t.Fatalf("failed to create tool: %v", err)
146+
}
147+
148+
mockExec := &MockExecutor{}
149+
clonedTool := tool.CloneWithExecutor(mockExec)
150+
151+
ctx := context.WithValue(context.Background(), WorkDirKey, "/tmp")
152+
args := map[string]any{
153+
"command": "hello",
154+
}
155+
156+
result, err := clonedTool.Run(ctx, args)
157+
if err != nil {
158+
t.Fatalf("tool run failed: %v", err)
159+
}
160+
161+
execResult, ok := result.(*sandbox.ExecResult)
162+
if !ok {
163+
t.Fatalf("expected *sandbox.ExecResult, got %T", result)
164+
}
165+
if execResult.Stdout != "executed" {
166+
t.Errorf("expected Stdout 'executed', got %q", execResult.Stdout)
167+
}
168+
169+
if mockExec.CapturedCommand != "echo hello" {
170+
t.Errorf("expected command 'echo hello', got %q", mockExec.CapturedCommand)
171+
}
172+
if !strings.Contains(strings.Join(mockExec.CapturedEnv, "\n"), "PATH") {
173+
t.Errorf("expected captured environment to contain PATH")
174+
}
175+
if mockExec.CapturedWorkDir != "/tmp" {
176+
t.Errorf("expected workdir '/tmp', got %q", mockExec.CapturedWorkDir)
177+
}
178+
}

pkg/tools/tools.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,25 @@ func (t *Tools) RegisterTool(tool Tool) {
9898
t.tools[name] = tool
9999
}
100100

101+
// CloneWithExecutor creates a shallow copy of the Tools collection,
102+
// but clones any tools that need a session-specific executor (like CustomTool).
103+
func (t *Tools) CloneWithExecutor(executor sandbox.Executor) Tools {
104+
newTools := Tools{
105+
tools: make(map[string]Tool),
106+
}
107+
108+
for name, tool := range t.tools {
109+
// If it's a CustomTool, we need to clone it with the session-specific executor
110+
if ct, ok := tool.(*CustomTool); ok {
111+
newTools.tools[name] = ct.CloneWithExecutor(executor)
112+
} else {
113+
// For other tools (like MCP tools), we reuse the existing instance
114+
newTools.tools[name] = tool
115+
}
116+
}
117+
return newTools
118+
}
119+
101120
type ToolCall struct {
102121
tool Tool
103122
name string

0 commit comments

Comments
 (0)