fix: resolve code review issues in tool-result-refactor

1. Remove duplicate ToolResult definition in heartbeat package
   - Import tools.ToolResult instead of local definition
   - Add nil check for handler before execution

2. Fix SpawnTool to return AsyncResult and implement AsyncTool
   - Add callback field and SetCallback method
   - Return AsyncResult instead of NewToolResult

3. Add context cancellation support to SubagentManager
   - Check ctx.Done() before and during task execution
   - Set task status to "cancelled" on cancellation
   - Call callback with result on completion

4. Fix data race window in CronTool.addJob
   - Use Lock instead of RLock for channel/chatID access
   - Ensure consistent snapshot during job creation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yinwm
2026-02-13 01:59:50 +08:00
parent e7e086155e
commit 474f3dbf90
5 changed files with 73 additions and 28 deletions

View File

@@ -16,28 +16,18 @@ import (
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools"
)
const (
minIntervalMinutes = 5
minIntervalMinutes = 5
defaultIntervalMinutes = 30
heartbeatOK = "HEARTBEAT_OK"
)
// ToolResult represents a structured result from tool execution.
// This is a minimal local definition to avoid circular dependencies.
type ToolResult struct {
ForLLM string `json:"for_llm"`
ForUser string `json:"for_user,omitempty"`
Silent bool `json:"silent"`
IsError bool `json:"is_error"`
Async bool `json:"async"`
Err error `json:"-"`
}
// HeartbeatHandler is the function type for handling heartbeat with tool support.
// It returns a ToolResult that can indicate async operations.
type HeartbeatHandler func(prompt string) *ToolResult
type HeartbeatHandler func(prompt string) *tools.ToolResult
// ChannelSender defines the interface for sending messages to channels.
// This is used to send heartbeat results back to the user.
@@ -213,6 +203,12 @@ func (hs *HeartbeatService) ExecuteHeartbeatWithTools(prompt string) {
// executeHeartbeatWithTools is the internal implementation of tool-supporting heartbeat.
func (hs *HeartbeatService) executeHeartbeatWithTools(prompt string) {
// Check if handler is configured
if hs.onHeartbeatWithTools == nil {
hs.logError("onHeartbeatWithTools handler not configured")
return
}
result := hs.onHeartbeatWithTools(prompt)
if result == nil {

View File

@@ -5,6 +5,8 @@ import (
"path/filepath"
"testing"
"time"
"github.com/sipeed/picoclaw/pkg/tools"
)
func TestExecuteHeartbeatWithTools_Async(t *testing.T) {
@@ -23,7 +25,7 @@ func TestExecuteHeartbeatWithTools_Async(t *testing.T) {
// Track if async handler was called
asyncCalled := false
asyncResult := &ToolResult{
asyncResult := &tools.ToolResult{
ForLLM: "Background task started",
ForUser: "Task started in background",
Silent: false,
@@ -31,7 +33,7 @@ func TestExecuteHeartbeatWithTools_Async(t *testing.T) {
Async: true,
}
hs.SetOnHeartbeatWithTools(func(prompt string) *ToolResult {
hs.SetOnHeartbeatWithTools(func(prompt string) *tools.ToolResult {
asyncCalled = true
if prompt == "" {
t.Error("Expected non-empty prompt")
@@ -61,7 +63,7 @@ func TestExecuteHeartbeatWithTools_Error(t *testing.T) {
hs := NewHeartbeatService(tmpDir, nil, 30, true)
errorResult := &ToolResult{
errorResult := &tools.ToolResult{
ForLLM: "Heartbeat failed: connection error",
ForUser: "",
Silent: false,
@@ -69,7 +71,7 @@ func TestExecuteHeartbeatWithTools_Error(t *testing.T) {
Async: false,
}
hs.SetOnHeartbeatWithTools(func(prompt string) *ToolResult {
hs.SetOnHeartbeatWithTools(func(prompt string) *tools.ToolResult {
return errorResult
})
@@ -101,7 +103,7 @@ func TestExecuteHeartbeatWithTools_Sync(t *testing.T) {
hs := NewHeartbeatService(tmpDir, nil, 30, true)
syncResult := &ToolResult{
syncResult := &tools.ToolResult{
ForLLM: "Heartbeat completed successfully",
ForUser: "",
Silent: true,
@@ -109,7 +111,7 @@ func TestExecuteHeartbeatWithTools_Sync(t *testing.T) {
Async: false,
}
hs.SetOnHeartbeatWithTools(func(prompt string) *ToolResult {
hs.SetOnHeartbeatWithTools(func(prompt string) *tools.ToolResult {
return syncResult
})
@@ -185,7 +187,7 @@ func TestExecuteHeartbeatWithTools_NilResult(t *testing.T) {
hs := NewHeartbeatService(tmpDir, nil, 30, true)
hs.SetOnHeartbeatWithTools(func(prompt string) *ToolResult {
hs.SetOnHeartbeatWithTools(func(prompt string) *tools.ToolResult {
return nil
})