update non tool access

This commit is contained in:
Wikig 2026-02-22 14:29:06 +08:00
parent b9a66248d8
commit 7b44498b84
9 changed files with 274 additions and 5 deletions

View file

@ -6,7 +6,8 @@
"model": "gpt4", "model": "gpt4",
"max_tokens": 8192, "max_tokens": 8192,
"temperature": 0.7, "temperature": 0.7,
"max_tool_iterations": 20 "max_tool_iterations": 20,
"enable_tools": true
} }
}, },
"model_list": [ "model_list": [

View file

@ -24,6 +24,7 @@ type AgentInstance struct {
MaxTokens int MaxTokens int
Temperature float64 Temperature float64
ContextWindow int ContextWindow int
EnableTools bool
Provider providers.LLMProvider Provider providers.LLMProvider
Sessions *session.SessionManager Sessions *session.SessionManager
ContextBuilder *ContextBuilder ContextBuilder *ContextBuilder
@ -45,6 +46,7 @@ func NewAgentInstance(
model := resolveAgentModel(agentCfg, defaults) model := resolveAgentModel(agentCfg, defaults)
fallbacks := resolveAgentFallbacks(agentCfg, defaults) fallbacks := resolveAgentFallbacks(agentCfg, defaults)
enableTools := resolveAgentEnableTools(agentCfg, defaults)
restrict := defaults.RestrictToWorkspace restrict := defaults.RestrictToWorkspace
toolsRegistry := tools.NewToolRegistry() toolsRegistry := tools.NewToolRegistry()
@ -105,6 +107,7 @@ func NewAgentInstance(
MaxTokens: maxTokens, MaxTokens: maxTokens,
Temperature: temperature, Temperature: temperature,
ContextWindow: maxTokens, ContextWindow: maxTokens,
EnableTools: enableTools,
Provider: provider, Provider: provider,
Sessions: sessionsManager, Sessions: sessionsManager,
ContextBuilder: contextBuilder, ContextBuilder: contextBuilder,
@ -144,6 +147,18 @@ func resolveAgentFallbacks(agentCfg *config.AgentConfig, defaults *config.AgentD
return defaults.ModelFallbacks return defaults.ModelFallbacks
} }
// resolveAgentEnableTools resolves whether tool calling is enabled for an agent.
func resolveAgentEnableTools(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) bool {
enabled := true
if defaults != nil && defaults.EnableTools != nil {
enabled = *defaults.EnableTools
}
if agentCfg != nil && agentCfg.EnableTools != nil {
enabled = *agentCfg.EnableTools
}
return enabled
}
func expandHome(path string) string { func expandHome(path string) string {
if path == "" { if path == "" {
return path return path

View file

@ -93,3 +93,92 @@ func TestNewAgentInstance_DefaultsTemperatureWhenUnset(t *testing.T) {
t.Fatalf("Temperature = %f, want %f", agent.Temperature, 0.7) t.Fatalf("Temperature = %f, want %f", agent.Temperature, 0.7)
} }
} }
func TestNewAgentInstance_EnableToolsDefaultsToTrue(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 1234,
MaxToolIterations: 5,
},
},
}
provider := &mockProvider{}
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, provider)
if !agent.EnableTools {
t.Fatal("EnableTools = false, want true")
}
}
func TestNewAgentInstance_EnableToolsFromDefaults(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
disabled := false
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 1234,
MaxToolIterations: 5,
EnableTools: &disabled,
},
},
}
provider := &mockProvider{}
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, provider)
if agent.EnableTools {
t.Fatal("EnableTools = true, want false")
}
}
func TestNewAgentInstance_EnableToolsAgentOverride(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
disabled := false
enabled := true
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 1234,
MaxToolIterations: 5,
EnableTools: &disabled,
},
},
}
agentCfg := &config.AgentConfig{
ID: "main",
Default: true,
EnableTools: &enabled,
}
provider := &mockProvider{}
agent := NewAgentInstance(agentCfg, &cfg.Agents.Defaults, cfg, provider)
if !agent.EnableTools {
t.Fatal("EnableTools = false, want true")
}
}

View file

@ -486,8 +486,11 @@ func (al *AgentLoop) runLLMIteration(
"max": agent.MaxIterations, "max": agent.MaxIterations,
}) })
// Build tool definitions // Build tool definitions (chat-only mode omits tools entirely).
providerToolDefs := agent.Tools.ToProviderDefs() var providerToolDefs []providers.ToolDefinition
if agent.EnableTools {
providerToolDefs = agent.Tools.ToProviderDefs()
}
// Log LLM request details // Log LLM request details
logger.DebugCF("agent", "LLM request", logger.DebugCF("agent", "LLM request",
@ -497,6 +500,7 @@ func (al *AgentLoop) runLLMIteration(
"model": agent.Model, "model": agent.Model,
"messages_count": len(messages), "messages_count": len(messages),
"tools_count": len(providerToolDefs), "tools_count": len(providerToolDefs),
"tools_enabled": agent.EnableTools,
"max_tokens": agent.MaxTokens, "max_tokens": agent.MaxTokens,
"temperature": agent.Temperature, "temperature": agent.Temperature,
"system_prompt_len": len(messages[0].Content), "system_prompt_len": len(messages[0].Content),
@ -590,6 +594,25 @@ func (al *AgentLoop) runLLMIteration(
return "", iteration, fmt.Errorf("LLM call failed after retries: %w", err) return "", iteration, fmt.Errorf("LLM call failed after retries: %w", err)
} }
if !agent.EnableTools {
if len(response.ToolCalls) > 0 {
logger.WarnCF("agent", "Ignoring tool calls in chat-only mode",
map[string]any{
"agent_id": agent.ID,
"iteration": iteration,
"tool_calls_received": len(response.ToolCalls),
})
}
finalContent = response.Content
logger.InfoCF("agent", "LLM response in chat-only mode",
map[string]any{
"agent_id": agent.ID,
"iteration": iteration,
"content_chars": len(finalContent),
})
break
}
// Check if no tool calls - we're done // Check if no tool calls - we're done
if len(response.ToolCalls) == 0 { if len(response.ToolCalls) == 0 {
finalContent = response.Content finalContent = response.Content

View file

@ -366,6 +366,29 @@ func (m *simpleMockProvider) GetDefaultModel() string {
return "mock-model" return "mock-model"
} }
type captureToolsProvider struct {
response string
lastToolsCount int
}
func (m *captureToolsProvider) Chat(
ctx context.Context,
messages []providers.Message,
tools []providers.ToolDefinition,
model string,
opts map[string]any,
) (*providers.LLMResponse, error) {
m.lastToolsCount = len(tools)
return &providers.LLMResponse{
Content: m.response,
ToolCalls: []providers.ToolCall{},
}, nil
}
func (m *captureToolsProvider) GetDefaultModel() string {
return "mock-capture-model"
}
// mockCustomTool is a simple mock tool for registration testing // mockCustomTool is a simple mock tool for registration testing
type mockCustomTool struct{} type mockCustomTool struct{}
@ -437,6 +460,92 @@ func (h testHelper) executeAndGetResponse(tb testing.TB, ctx context.Context, ms
const responseTimeout = 3 * time.Second const responseTimeout = 3 * time.Second
func TestAgentLoop_ChatOnlyMode_DoesNotPassTools(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
disabled := false
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 4096,
MaxToolIterations: 10,
EnableTools: &disabled,
},
},
}
msgBus := bus.NewMessageBus()
provider := &captureToolsProvider{response: "chat-only"}
al := NewAgentLoop(cfg, msgBus, provider)
response, err := al.ProcessDirectWithChannel(
context.Background(),
"hello",
"chat-only-session",
"test",
"chat",
)
if err != nil {
t.Fatalf("ProcessDirectWithChannel() error = %v", err)
}
if response != "chat-only" {
t.Fatalf("response = %q, want %q", response, "chat-only")
}
if provider.lastToolsCount != 0 {
t.Fatalf("lastToolsCount = %d, want 0", provider.lastToolsCount)
}
}
func TestAgentLoop_ToolsEnabled_PassesTools(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
enabled := true
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 4096,
MaxToolIterations: 10,
EnableTools: &enabled,
},
},
}
msgBus := bus.NewMessageBus()
provider := &captureToolsProvider{response: "with-tools"}
al := NewAgentLoop(cfg, msgBus, provider)
response, err := al.ProcessDirectWithChannel(
context.Background(),
"hello",
"with-tools-session",
"test",
"chat",
)
if err != nil {
t.Fatalf("ProcessDirectWithChannel() error = %v", err)
}
if response != "with-tools" {
t.Fatalf("response = %q, want %q", response, "with-tools")
}
if provider.lastToolsCount == 0 {
t.Fatal("lastToolsCount = 0, want > 0")
}
}
// TestToolResult_SilentToolDoesNotSendUserMessage verifies silent tools don't trigger outbound // TestToolResult_SilentToolDoesNotSendUserMessage verifies silent tools don't trigger outbound
func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) { func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*") tmpDir, err := os.MkdirTemp("", "agent-test-*")

View file

@ -134,6 +134,7 @@ type AgentConfig struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Workspace string `json:"workspace,omitempty"` Workspace string `json:"workspace,omitempty"`
Model *AgentModelConfig `json:"model,omitempty"` Model *AgentModelConfig `json:"model,omitempty"`
EnableTools *bool `json:"enable_tools,omitempty"`
Skills []string `json:"skills,omitempty"` Skills []string `json:"skills,omitempty"`
Subagents *SubagentsConfig `json:"subagents,omitempty"` Subagents *SubagentsConfig `json:"subagents,omitempty"`
} }
@ -177,6 +178,7 @@ type AgentDefaults struct {
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
EnableTools *bool `json:"enable_tools,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_ENABLE_TOOLS"`
} }
type ChannelsConfig struct { type ChannelsConfig struct {

View file

@ -69,14 +69,16 @@ func TestAgentConfig_FullParse(t *testing.T) {
"workspace": "~/.picoclaw/workspace", "workspace": "~/.picoclaw/workspace",
"model": "glm-4.7", "model": "glm-4.7",
"max_tokens": 8192, "max_tokens": 8192,
"max_tool_iterations": 20 "max_tool_iterations": 20,
"enable_tools": true
}, },
"list": [ "list": [
{ {
"id": "sales", "id": "sales",
"default": true, "default": true,
"name": "Sales Bot", "name": "Sales Bot",
"model": "gpt-4" "model": "gpt-4",
"enable_tools": false
}, },
{ {
"id": "support", "id": "support",
@ -125,6 +127,9 @@ func TestAgentConfig_FullParse(t *testing.T) {
if sales.Model == nil || sales.Model.Primary != "gpt-4" { if sales.Model == nil || sales.Model.Primary != "gpt-4" {
t.Errorf("sales.Model = %+v", sales.Model) t.Errorf("sales.Model = %+v", sales.Model)
} }
if sales.EnableTools == nil || *sales.EnableTools {
t.Errorf("sales.EnableTools = %v, want false", sales.EnableTools)
}
support := cfg.Agents.List[1] support := cfg.Agents.List[1]
if support.ID != "support" || support.Name != "Support Bot" { if support.ID != "support" || support.Name != "Support Bot" {
@ -161,6 +166,9 @@ func TestAgentConfig_FullParse(t *testing.T) {
if len(links) != 2 { if len(links) != 2 {
t.Errorf("john links = %v", links) t.Errorf("john links = %v", links)
} }
if cfg.Agents.Defaults.EnableTools == nil || !*cfg.Agents.Defaults.EnableTools {
t.Errorf("Agents.Defaults.EnableTools = %v, want true", cfg.Agents.Defaults.EnableTools)
}
} }
func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) { func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) {
@ -186,6 +194,9 @@ func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) {
if len(cfg.Bindings) != 0 { if len(cfg.Bindings) != 0 {
t.Errorf("bindings should be empty, got %d", len(cfg.Bindings)) t.Errorf("bindings should be empty, got %d", len(cfg.Bindings))
} }
if cfg.Agents.Defaults.EnableTools == nil || !*cfg.Agents.Defaults.EnableTools {
t.Errorf("Agents.Defaults.EnableTools = %v, want true", cfg.Agents.Defaults.EnableTools)
}
} }
// TestDefaultConfig_HeartbeatEnabled verifies heartbeat is enabled by default // TestDefaultConfig_HeartbeatEnabled verifies heartbeat is enabled by default
@ -233,6 +244,17 @@ func TestDefaultConfig_MaxToolIterations(t *testing.T) {
} }
} }
func TestDefaultConfig_EnableTools(t *testing.T) {
cfg := DefaultConfig()
if cfg.Agents.Defaults.EnableTools == nil {
t.Fatal("EnableTools should not be nil")
}
if !*cfg.Agents.Defaults.EnableTools {
t.Error("EnableTools should default to true")
}
}
// TestDefaultConfig_Temperature verifies temperature has default value // TestDefaultConfig_Temperature verifies temperature has default value
func TestDefaultConfig_Temperature(t *testing.T) { func TestDefaultConfig_Temperature(t *testing.T) {
cfg := DefaultConfig() cfg := DefaultConfig()

View file

@ -17,6 +17,7 @@ func DefaultConfig() *Config {
MaxTokens: 8192, MaxTokens: 8192,
Temperature: nil, // nil means use provider default Temperature: nil, // nil means use provider default
MaxToolIterations: 20, MaxToolIterations: 20,
EnableTools: boolPtr(true),
}, },
}, },
Bindings: []AgentBinding{}, Bindings: []AgentBinding{},
@ -314,3 +315,7 @@ func DefaultConfig() *Config {
}, },
} }
} }
func boolPtr(v bool) *bool {
return &v
}

View file

@ -84,6 +84,9 @@ func ConvertConfig(data map[string]any) (*config.Config, []string, error) {
if v, ok := getFloat(defaults, "max_tool_iterations"); ok { if v, ok := getFloat(defaults, "max_tool_iterations"); ok {
cfg.Agents.Defaults.MaxToolIterations = int(v) cfg.Agents.Defaults.MaxToolIterations = int(v)
} }
if v, ok := getBool(defaults, "enable_tools"); ok {
cfg.Agents.Defaults.EnableTools = &v
}
if v, ok := getString(defaults, "workspace"); ok { if v, ok := getString(defaults, "workspace"); ok {
cfg.Agents.Defaults.Workspace = rewriteWorkspacePath(v) cfg.Agents.Defaults.Workspace = rewriteWorkspacePath(v)
} }