fix: make forceCompression snap cut point to tool pair boundaries
The midpoint calculation now walks forward past tool_result messages to avoid splitting an assistant+tool_calls from its tool_results.
This commit is contained in:
parent
3d1d480975
commit
04d047fd37
2 changed files with 82 additions and 0 deletions
|
|
@ -811,6 +811,16 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
|
||||||
// Helper to find the mid-point of the conversation
|
// Helper to find the mid-point of the conversation
|
||||||
mid := len(conversation) / 2
|
mid := len(conversation) / 2
|
||||||
|
|
||||||
|
// Snap the cut point forward past any tool_result messages so we don't
|
||||||
|
// separate them from their assistant+tool_calls header.
|
||||||
|
for mid < len(conversation) && conversation[mid].Role == "tool" {
|
||||||
|
mid++
|
||||||
|
}
|
||||||
|
// Safety: if we walked past the end, step back to keep at least 1 message
|
||||||
|
if mid >= len(conversation) {
|
||||||
|
mid = len(conversation) - 1
|
||||||
|
}
|
||||||
|
|
||||||
// New history structure:
|
// New history structure:
|
||||||
// 1. System Prompt (with compression note appended)
|
// 1. System Prompt (with compression note appended)
|
||||||
// 2. Second half of conversation
|
// 2. Second half of conversation
|
||||||
|
|
|
||||||
|
|
@ -858,6 +858,78 @@ func TestHandleCommand_New(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForceCompression_PreservesToolPairs(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,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &mockProvider{}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
agent := al.registry.GetDefaultAgent()
|
||||||
|
if agent == nil {
|
||||||
|
t.Fatal("No default agent found")
|
||||||
|
}
|
||||||
|
sessionKey := "agent:main:test-compression"
|
||||||
|
agent.Sessions.GetOrCreate(sessionKey)
|
||||||
|
|
||||||
|
// Build: system, user, assistant+tool, tool_result, tool_result, user, assistant, user
|
||||||
|
// = 8 messages. Conversation (indices 1-6) has 6 msgs, mid=3 lands on
|
||||||
|
// the second tool_result, splitting it from its assistant+tool_calls.
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, providers.Message{Role: "system", Content: "sys"})
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, providers.Message{Role: "user", Content: "q1"})
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, providers.Message{
|
||||||
|
Role: "assistant", Content: "a1",
|
||||||
|
ToolCalls: []providers.ToolCall{{ID: "c1", Name: "exec"}, {ID: "c2", Name: "read"}},
|
||||||
|
})
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, providers.Message{Role: "tool", Content: "r1", ToolCallID: "c1"})
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, providers.Message{Role: "tool", Content: "r2", ToolCallID: "c2"})
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, providers.Message{Role: "user", Content: "q2"})
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, providers.Message{Role: "assistant", Content: "a2"})
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, providers.Message{Role: "user", Content: "q3"})
|
||||||
|
|
||||||
|
al.forceCompression(agent, sessionKey)
|
||||||
|
|
||||||
|
history := agent.Sessions.GetHistory(sessionKey)
|
||||||
|
|
||||||
|
toolCallIDs := map[string]bool{}
|
||||||
|
toolResultIDs := map[string]bool{}
|
||||||
|
for _, m := range history {
|
||||||
|
for _, tc := range m.ToolCalls {
|
||||||
|
if tc.ID != "" {
|
||||||
|
toolCallIDs[tc.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Role == "tool" && m.ToolCallID != "" {
|
||||||
|
toolResultIDs[m.ToolCallID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id := range toolResultIDs {
|
||||||
|
if !toolCallIDs[id] {
|
||||||
|
t.Errorf("orphaned tool_result %q after forceCompression", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id := range toolCallIDs {
|
||||||
|
if !toolResultIDs[id] {
|
||||||
|
t.Errorf("orphaned tool_call %q after forceCompression", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleCommand_Status(t *testing.T) {
|
func TestHandleCommand_Status(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue