fix(agent): suppress heartbeat tool feedback (#1937)
This commit is contained in:
parent
3d20976803
commit
85dfb341a8
2 changed files with 153 additions and 9 deletions
|
|
@ -85,6 +85,7 @@ type processOptions struct {
|
||||||
DefaultResponse string // Response when LLM returns empty
|
DefaultResponse string // Response when LLM returns empty
|
||||||
EnableSummary bool // Whether to trigger summarization
|
EnableSummary bool // Whether to trigger summarization
|
||||||
SendResponse bool // Whether to send response via bus
|
SendResponse bool // Whether to send response via bus
|
||||||
|
SuppressToolFeedback bool // Whether to suppress inline tool feedback messages
|
||||||
NoHistory bool // If true, don't load session history (for heartbeat)
|
NoHistory bool // If true, don't load session history (for heartbeat)
|
||||||
SkipInitialSteeringPoll bool // If true, skip the steering poll at loop start (used by Continue)
|
SkipInitialSteeringPoll bool // If true, skip the steering poll at loop start (used by Continue)
|
||||||
}
|
}
|
||||||
|
|
@ -1242,14 +1243,15 @@ func (al *AgentLoop) ProcessHeartbeat(
|
||||||
return "", fmt.Errorf("no default agent for heartbeat")
|
return "", fmt.Errorf("no default agent for heartbeat")
|
||||||
}
|
}
|
||||||
return al.runAgentLoop(ctx, agent, processOptions{
|
return al.runAgentLoop(ctx, agent, processOptions{
|
||||||
SessionKey: "heartbeat",
|
SessionKey: "heartbeat",
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
ChatID: chatID,
|
ChatID: chatID,
|
||||||
UserMessage: content,
|
UserMessage: content,
|
||||||
DefaultResponse: defaultResponse,
|
DefaultResponse: defaultResponse,
|
||||||
EnableSummary: false,
|
EnableSummary: false,
|
||||||
SendResponse: false,
|
SendResponse: false,
|
||||||
NoHistory: true, // Don't load session history for heartbeat
|
SuppressToolFeedback: true,
|
||||||
|
NoHistory: true, // Don't load session history for heartbeat
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2305,7 +2307,9 @@ turnLoop:
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send tool feedback to chat channel if enabled (from HEAD)
|
// Send tool feedback to chat channel if enabled (from HEAD)
|
||||||
if al.cfg.Agents.Defaults.IsToolFeedbackEnabled() && ts.channel != "" {
|
if al.cfg.Agents.Defaults.IsToolFeedbackEnabled() &&
|
||||||
|
ts.channel != "" &&
|
||||||
|
!ts.opts.SuppressToolFeedback {
|
||||||
feedbackPreview := utils.Truncate(
|
feedbackPreview := utils.Truncate(
|
||||||
string(argsJSON),
|
string(argsJSON),
|
||||||
al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength(),
|
al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength(),
|
||||||
|
|
|
||||||
|
|
@ -1018,6 +1018,40 @@ func (m *artifactThenSendProvider) GetDefaultModel() string {
|
||||||
return "artifact-then-send-model"
|
return "artifact-then-send-model"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type toolFeedbackProvider struct {
|
||||||
|
filePath string
|
||||||
|
calls int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *toolFeedbackProvider) Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []providers.Message,
|
||||||
|
tools []providers.ToolDefinition,
|
||||||
|
model string,
|
||||||
|
opts map[string]any,
|
||||||
|
) (*providers.LLMResponse, error) {
|
||||||
|
m.calls++
|
||||||
|
if m.calls == 1 {
|
||||||
|
return &providers.LLMResponse{
|
||||||
|
ToolCalls: []providers.ToolCall{{
|
||||||
|
ID: "call_heartbeat_read_file",
|
||||||
|
Type: "function",
|
||||||
|
Name: "read_file",
|
||||||
|
Arguments: map[string]any{"path": m.filePath},
|
||||||
|
}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &providers.LLMResponse{
|
||||||
|
Content: "HEARTBEAT_OK",
|
||||||
|
ToolCalls: []providers.ToolCall{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *toolFeedbackProvider) GetDefaultModel() string {
|
||||||
|
return "heartbeat-tool-feedback-model"
|
||||||
|
}
|
||||||
|
|
||||||
type toolLimitOnlyProvider struct{}
|
type toolLimitOnlyProvider struct{}
|
||||||
|
|
||||||
func (m *toolLimitOnlyProvider) Chat(
|
func (m *toolLimitOnlyProvider) Chat(
|
||||||
|
|
@ -2313,6 +2347,112 @@ func TestProcessMessage_PublishesReasoningContentToReasoningChannel(t *testing.T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProcessHeartbeat_DoesNotPublishToolFeedback(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
heartbeatFile := filepath.Join(tmpDir, "heartbeat-task.txt")
|
||||||
|
if err := os.WriteFile(heartbeatFile, []byte("heartbeat task"), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
ModelName: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
ToolFeedback: config.ToolFeedbackConfig{
|
||||||
|
Enabled: true,
|
||||||
|
MaxArgsLength: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tools: config.ToolsConfig{
|
||||||
|
ReadFile: config.ReadFileToolConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &toolFeedbackProvider{filePath: heartbeatFile}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
response, err := al.ProcessHeartbeat(context.Background(), "check heartbeat tasks", "telegram", "chat-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessHeartbeat() error = %v", err)
|
||||||
|
}
|
||||||
|
if response != "HEARTBEAT_OK" {
|
||||||
|
t.Fatalf("ProcessHeartbeat() response = %q, want %q", response, "HEARTBEAT_OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case outbound := <-msgBus.OutboundChan():
|
||||||
|
t.Fatalf("expected no outbound tool feedback during heartbeat, got %+v", outbound)
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_PublishesToolFeedbackWhenEnabled(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
heartbeatFile := filepath.Join(tmpDir, "tool-feedback.txt")
|
||||||
|
if err := os.WriteFile(heartbeatFile, []byte("tool feedback task"), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
ModelName: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
ToolFeedback: config.ToolFeedbackConfig{
|
||||||
|
Enabled: true,
|
||||||
|
MaxArgsLength: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tools: config.ToolsConfig{
|
||||||
|
ReadFile: config.ReadFileToolConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &toolFeedbackProvider{filePath: heartbeatFile}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
response, err := al.processMessage(context.Background(), bus.InboundMessage{
|
||||||
|
Channel: "telegram",
|
||||||
|
SenderID: "user-1",
|
||||||
|
ChatID: "chat-1",
|
||||||
|
Content: "check tool feedback",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("processMessage() error = %v", err)
|
||||||
|
}
|
||||||
|
if response != "HEARTBEAT_OK" {
|
||||||
|
t.Fatalf("processMessage() response = %q, want %q", response, "HEARTBEAT_OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case outbound := <-msgBus.OutboundChan():
|
||||||
|
if outbound.Channel != "telegram" {
|
||||||
|
t.Fatalf("tool feedback channel = %q, want %q", outbound.Channel, "telegram")
|
||||||
|
}
|
||||||
|
if outbound.ChatID != "chat-1" {
|
||||||
|
t.Fatalf("tool feedback chatID = %q, want %q", outbound.ChatID, "chat-1")
|
||||||
|
}
|
||||||
|
if !strings.Contains(outbound.Content, "`read_file`") {
|
||||||
|
t.Fatalf("tool feedback content = %q, want read_file preview", outbound.Content)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("expected outbound tool feedback for regular messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveMediaRefs_ResolvesToBase64(t *testing.T) {
|
func TestResolveMediaRefs_ResolvesToBase64(t *testing.T) {
|
||||||
store := media.NewFileMediaStore()
|
store := media.NewFileMediaStore()
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue