Files
viettranx 7d35ee53c7 fix(agent): smart delegation prompt + compaction pending state + block team tools in subagents
- Leader prompt: replace blanket "prefer delegation" with conditional —
  delegate complex work, handle simple requests directly
- Compaction prompt: explicitly preserve pending subagent/team task state
  and "waiting for" expectations across summarization
- Add team_tasks to SubagentDenyAlways — subagents must not use team
  orchestration tools

Closes partially #600
2026-03-31 11:44:54 +07:00

119 lines
3.7 KiB
Go

package agent
import (
"context"
"fmt"
"log/slog"
"strings"
"time"
"github.com/nextlevelbuilder/goclaw/internal/providers"
)
// compactionSummaryPrompt is the structured summarization instruction used by both
// mid-loop compaction and background summarization. Matching OpenClaw TS compaction.ts
// MERGE_SUMMARIES_INSTRUCTIONS + IDENTIFIER_PRESERVATION_INSTRUCTIONS.
const compactionSummaryPrompt = `Summarize this conversation concisely for the AI agent to resume work.
MUST PRESERVE:
- Active tasks and their current status (in-progress, blocked, pending)
- Pending subagent tasks (IDs, labels, statuses) — agent needs to know what is still running
- Pending team task results awaiting delivery (task IDs, assignees, statuses)
- Any "waiting for..." state — do NOT drop expectations of future results
- Batch operation progress (e.g., "5/17 items completed")
- The last thing the user requested and what was being done about it
- Decisions made and their rationale
- TODOs, open questions, and constraints
- Any commitments or follow-ups promised
IDENTIFIER PRESERVATION:
Preserve all opaque identifiers exactly as written (no shortening or reconstruction),
including UUIDs, hashes, IDs, tokens, API keys, hostnames, IPs, ports, URLs, and file names.
PRIORITIZE recent context over older history. The agent needs to know
what it was doing, not just what was discussed.
Conversation to summarize:
`
// compactMessagesInPlace summarizes the first ~70% of messages into a condensed
// summary, keeping the last ~30% intact. Operates purely on the local messages
// slice — no session state touched, no locks needed.
// Returns nil on failure (caller keeps original messages).
func (l *Loop) compactMessagesInPlace(ctx context.Context, messages []providers.Message) []providers.Message {
if len(messages) < 6 {
return nil
}
// Resolve keepCount from compaction config (same defaults as maybeSummarize).
keepCount := 4
if l.compactionCfg != nil && l.compactionCfg.KeepLastMessages > 0 {
keepCount = l.compactionCfg.KeepLastMessages
}
// Ensure we keep at least 30% of messages.
if minKeep := len(messages) * 3 / 10; minKeep > keepCount {
keepCount = minKeep
}
splitIdx := len(messages) - keepCount
// Walk backward from splitIdx to find a clean boundary —
// avoid splitting tool_use → tool_result pairs.
for splitIdx > 0 {
m := messages[splitIdx]
if m.Role == "tool" || (m.Role == "assistant" && len(m.ToolCalls) > 0) {
splitIdx--
continue
}
break
}
if splitIdx <= 1 {
return nil
}
// Build summary input (same pattern as maybeSummarize in loop_history.go).
toSummarize := messages[:splitIdx]
var sb strings.Builder
for _, m := range toSummarize {
switch m.Role {
case "user":
fmt.Fprintf(&sb, "user: %s\n", m.Content)
case "assistant":
fmt.Fprintf(&sb, "assistant: %s\n", SanitizeAssistantContent(m.Content))
}
}
sctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
resp, err := l.provider.Chat(sctx, providers.ChatRequest{
Messages: []providers.Message{{
Role: "user",
Content: compactionSummaryPrompt + sb.String(),
}},
Model: l.model,
Options: map[string]any{"max_tokens": 1024, "temperature": 0.3},
})
if err != nil {
slog.Warn("mid_loop_compaction_failed", "agent", l.id, "error", err)
return nil
}
summary := providers.Message{
Role: "user",
Content: "[Summary of earlier conversation]\n" + SanitizeAssistantContent(resp.Content),
}
result := make([]providers.Message, 0, 1+keepCount)
result = append(result, summary)
result = append(result, messages[splitIdx:]...)
slog.Info("mid_loop_compacted",
"agent", l.id,
"original_msgs", len(messages),
"summarized", splitIdx,
"kept", len(result))
return result
}