diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 5c257539b..d62b9268f 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -264,11 +264,18 @@ func (al *AgentLoop) SetHooks(h *hooks.HookRegistry) error { // SetPluginManager installs a plugin manager and routes its hook registry into the loop. // Must be called before Run starts. func (al *AgentLoop) SetPluginManager(pm *plugin.Manager) error { - al.pluginManager = pm if pm == nil { - return al.SetHooks(nil) + if err := al.SetHooks(nil); err != nil { + return err + } + al.pluginManager = nil + return nil } - return al.SetHooks(pm.HookRegistry()) + if err := al.SetHooks(pm.HookRegistry()); err != nil { + return err + } + al.pluginManager = pm + return nil } // EnablePlugins is a convenience helper to build and install a plugin manager. @@ -848,13 +855,17 @@ func (al *AgentLoop) runLLMIteration( toolResult = tools.ErrorResult(reason) } tc.Arguments = btcEvent.Args + if tc.Arguments == nil { + tc.Arguments = make(map[string]any) + } } - toolStart := time.Now() + var toolDuration time.Duration if !toolCanceled { + toolStart := time.Now() toolResult = agent.Tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback) + toolDuration = time.Since(toolStart) } - toolDuration := time.Since(toolStart) // Fire after_tool_call hook (fires for both executed and canceled calls) if al.hooks != nil { diff --git a/pkg/agent/plugin_test.go b/pkg/agent/plugin_test.go index d0336956c..115923476 100644 --- a/pkg/agent/plugin_test.go +++ b/pkg/agent/plugin_test.go @@ -11,6 +11,8 @@ import ( "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/hooks" "github.com/sipeed/picoclaw/pkg/plugin" + "github.com/sipeed/picoclaw/pkg/providers" + "github.com/sipeed/picoclaw/pkg/tools" ) type blockingPlugin struct{} @@ -32,6 +34,68 @@ func (p blockingPlugin) Register(r *hooks.HookRegistry) error { return nil } +type nilArgsProvider struct { + calls int +} + +func (p *nilArgsProvider) Chat( + _ context.Context, + _ []providers.Message, + _ []providers.ToolDefinition, + _ string, + _ map[string]any, +) (*providers.LLMResponse, error) { + if p.calls == 0 { + p.calls++ + return &providers.LLMResponse{ + Content: "", + ToolCalls: []providers.ToolCall{ + { + ID: "tc-1", + Type: "function", + Name: "nil_args_tool", + Arguments: map[string]any{"seed": "value"}, + }, + }, + }, nil + } + p.calls++ + return &providers.LLMResponse{ + Content: "done", + ToolCalls: []providers.ToolCall{}, + }, nil +} + +func (p *nilArgsProvider) GetDefaultModel() string { + return "test-model" +} + +type nilArgsCaptureTool struct { + receivedNil bool +} + +func (t *nilArgsCaptureTool) Name() string { + return "nil_args_tool" +} + +func (t *nilArgsCaptureTool) Description() string { + return "captures whether args are nil" +} + +func (t *nilArgsCaptureTool) Parameters() map[string]any { + return map[string]any{ + "type": "object", + "properties": map[string]any{}, + } +} + +func (t *nilArgsCaptureTool) Execute(_ context.Context, args map[string]any) *tools.ToolResult { + if args == nil { + t.receivedNil = true + } + return tools.SilentResult("ok") +} + func TestSetPluginManagerInstallsHookRegistry(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-plugin-test-*") if err != nil {