fixes
This commit is contained in:
parent
d289d68091
commit
999bf20144
5 changed files with 143 additions and 2 deletions
|
|
@ -111,6 +111,7 @@ type continuationTarget struct {
|
||||||
const (
|
const (
|
||||||
defaultResponse = "The model returned an empty response. This may indicate a provider error or token limit."
|
defaultResponse = "The model returned an empty response. This may indicate a provider error or token limit."
|
||||||
toolLimitResponse = "I've reached `max_tool_iterations` without a final response. Increase `max_tool_iterations` in config.json if this task needs more tool steps."
|
toolLimitResponse = "I've reached `max_tool_iterations` without a final response. Increase `max_tool_iterations` in config.json if this task needs more tool steps."
|
||||||
|
toolRepeatLoopResponse = "Detected repeated tool calls without progress; stopping to avoid an infinite loop."
|
||||||
handledToolResponseSummary = "Requested output delivered via tool attachment."
|
handledToolResponseSummary = "Requested output delivered via tool attachment."
|
||||||
sessionKeyAgentPrefix = "agent::"
|
sessionKeyAgentPrefix = "agent::"
|
||||||
metadataKeyAccountID = "account_id"
|
metadataKeyAccountID = "account_id"
|
||||||
|
|
@ -1914,6 +1915,9 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, er
|
||||||
}
|
}
|
||||||
pendingMessages := append([]providers.Message(nil), ts.opts.InitialSteeringMessages...)
|
pendingMessages := append([]providers.Message(nil), ts.opts.InitialSteeringMessages...)
|
||||||
var finalContent string
|
var finalContent string
|
||||||
|
lastToolCallsFingerprint := ""
|
||||||
|
consecutiveRepeatedToolCalls := 0
|
||||||
|
const maxConsecutiveRepeatedToolCalls = 3
|
||||||
|
|
||||||
turnLoop:
|
turnLoop:
|
||||||
for ts.currentIteration() < ts.agent.MaxIterations || len(pendingMessages) > 0 || func() bool {
|
for ts.currentIteration() < ts.agent.MaxIterations || len(pendingMessages) > 0 || func() bool {
|
||||||
|
|
@ -2411,6 +2415,53 @@ turnLoop:
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Guardrail: if the model keeps requesting the exact same tool calls
|
||||||
|
// over and over (often due to missing/filtered tool results), stop
|
||||||
|
// early instead of running until max_tool_iterations.
|
||||||
|
type toolCallFP struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Args json.RawMessage `json:"args"`
|
||||||
|
}
|
||||||
|
fpParts := make([]toolCallFP, 0, len(normalizedToolCalls))
|
||||||
|
fingerprintBytes := make([]byte, 0)
|
||||||
|
for _, tc := range normalizedToolCalls {
|
||||||
|
argsJSON, err := json.Marshal(tc.Arguments)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fpParts = append(fpParts, toolCallFP{
|
||||||
|
Name: tc.Name,
|
||||||
|
Args: json.RawMessage(argsJSON),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(fpParts) > 0 {
|
||||||
|
if fp, err := json.Marshal(fpParts); err == nil {
|
||||||
|
fingerprintBytes = fp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fingerprintBytes) > 0 {
|
||||||
|
toolCallsFingerprint := string(fingerprintBytes)
|
||||||
|
if toolCallsFingerprint == lastToolCallsFingerprint {
|
||||||
|
consecutiveRepeatedToolCalls++
|
||||||
|
} else {
|
||||||
|
lastToolCallsFingerprint = toolCallsFingerprint
|
||||||
|
consecutiveRepeatedToolCalls = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if consecutiveRepeatedToolCalls >= maxConsecutiveRepeatedToolCalls {
|
||||||
|
turnStatus = TurnEndStatusError
|
||||||
|
finalContent = toolRepeatLoopResponse
|
||||||
|
logger.WarnCF("agent", "Stopping repeated tool call loop",
|
||||||
|
map[string]any{
|
||||||
|
"agent_id": ts.agent.ID,
|
||||||
|
"fingerprint_repeats": consecutiveRepeatedToolCalls,
|
||||||
|
"tools": toolNames,
|
||||||
|
"iteration": iteration,
|
||||||
|
})
|
||||||
|
break turnLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
allResponsesHandled := len(normalizedToolCalls) > 0
|
allResponsesHandled := len(normalizedToolCalls) > 0
|
||||||
assistantMsg := providers.Message{
|
assistantMsg := providers.Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
|
|
|
||||||
|
|
@ -2113,6 +2113,46 @@ func TestAgentLoop_ToolLimitUsesDedicatedFallback(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgentLoop_ToolRepeatLoopBreaksEarly(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)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
ModelName: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
// Keep this high so the loop-breaker (not the iteration limit)
|
||||||
|
// is what terminates the turn.
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &toolLimitOnlyProvider{}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
al.RegisterTool(&toolLimitTestTool{})
|
||||||
|
|
||||||
|
response, err := al.ProcessDirectWithChannel(
|
||||||
|
context.Background(),
|
||||||
|
"hello",
|
||||||
|
"tool-repeat-loop",
|
||||||
|
"test",
|
||||||
|
"direct",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||||
|
}
|
||||||
|
if response != toolRepeatLoopResponse {
|
||||||
|
t.Fatalf("response = %q, want %q", response, toolRepeatLoopResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestProcessDirectWithChannel_TriggersMCPInitialization verifies that
|
// TestProcessDirectWithChannel_TriggersMCPInitialization verifies that
|
||||||
// ProcessDirectWithChannel triggers MCP initialization when MCP is enabled.
|
// ProcessDirectWithChannel triggers MCP initialization when MCP is enabled.
|
||||||
// Note: Manager is only initialized when at least one MCP server is configured
|
// Note: Manager is only initialized when at least one MCP server is configured
|
||||||
|
|
|
||||||
45
pkg/channels/http/http.go
Normal file
45
pkg/channels/http/http.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/channels"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
channels.RegisterFactory("http", NewHTTPChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPChannel struct {
|
||||||
|
*channels.BaseChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPChannel(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) {
|
||||||
|
bc := channels.NewBaseChannel("http", nil, b, nil)
|
||||||
|
return &HTTPChannel{
|
||||||
|
BaseChannel: bc,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPChannel) Start(ctx context.Context) error {
|
||||||
|
c.SetRunning(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPChannel) Stop(ctx context.Context) error {
|
||||||
|
c.SetRunning(false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
|
logger.InfoCF("channels", "HTTP channel received outbound message", map[string]any{
|
||||||
|
"chat_id": msg.ChatID,
|
||||||
|
"content": msg.Content,
|
||||||
|
})
|
||||||
|
// For synchronous HTTP, the response is usually handled by the caller of ProcessDirectWithChannel.
|
||||||
|
// Asynchronous messages (e.g. from subagents) will just be logged here for now.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -430,6 +430,9 @@ func (m *Manager) initChannels(channels *config.ChannelsConfig) error {
|
||||||
m.initChannel("vk", "VK")
|
m.initChannel("vk", "VK")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always initialize HTTP channel as it is used for synchronous gateway chat
|
||||||
|
m.initChannel("http", "HTTP")
|
||||||
|
|
||||||
logger.InfoCF("channels", "Channel initialization completed", map[string]any{
|
logger.InfoCF("channels", "Channel initialization completed", map[string]any{
|
||||||
"enabled_channels": len(m.channels),
|
"enabled_channels": len(m.channels),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/dingtalk"
|
_ "github.com/sipeed/picoclaw/pkg/channels/dingtalk"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/discord"
|
_ "github.com/sipeed/picoclaw/pkg/channels/discord"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/feishu"
|
_ "github.com/sipeed/picoclaw/pkg/channels/feishu"
|
||||||
|
_ "github.com/sipeed/picoclaw/pkg/channels/http"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/irc"
|
_ "github.com/sipeed/picoclaw/pkg/channels/irc"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/line"
|
_ "github.com/sipeed/picoclaw/pkg/channels/line"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/maixcam"
|
_ "github.com/sipeed/picoclaw/pkg/channels/maixcam"
|
||||||
|
|
@ -227,10 +228,11 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error
|
||||||
if cfg.Gateway.ChatEnabled {
|
if cfg.Gateway.ChatEnabled {
|
||||||
runningServices.HealthServer.SetChatFunc(func(ctx context.Context, message, sessionID, chatID string) (string, error) {
|
runningServices.HealthServer.SetChatFunc(func(ctx context.Context, message, sessionID, chatID string) (string, error) {
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
sessionID = "http-chat"
|
sessionID = fmt.Sprintf("chat-%s", time.Now().Format("20060102-150405"))
|
||||||
}
|
}
|
||||||
if chatID == "" {
|
if chatID == "" {
|
||||||
chatID = "chat"
|
// Default to sessionID to ensure isolation
|
||||||
|
chatID = sessionID
|
||||||
}
|
}
|
||||||
return agentLoop.ProcessDirectWithChannel(ctx, message, sessionID, "http", chatID)
|
return agentLoop.ProcessDirectWithChannel(ctx, message, sessionID, "http", chatID)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue