diff --git a/internal/agent/loop_compact.go b/internal/agent/loop_compact.go index 108a00d3..489ceba7 100644 --- a/internal/agent/loop_compact.go +++ b/internal/agent/loop_compact.go @@ -10,6 +10,30 @@ import ( "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) +- 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. @@ -63,7 +87,7 @@ func (l *Loop) compactMessagesInPlace(ctx context.Context, messages []providers. resp, err := l.provider.Chat(sctx, providers.ChatRequest{ Messages: []providers.Message{{ Role: "user", - Content: "Provide a concise summary of this conversation, preserving key findings, data, and context:\n\n" + sb.String(), + Content: compactionSummaryPrompt + sb.String(), }}, Model: l.model, Options: map[string]any{"max_tokens": 1024, "temperature": 0.3}, diff --git a/internal/agent/loop_history.go b/internal/agent/loop_history.go index 76391540..97629531 100644 --- a/internal/agent/loop_history.go +++ b/internal/agent/loop_history.go @@ -617,14 +617,14 @@ func (l *Loop) maybeSummarize(ctx context.Context, sessionKey string) { } var prompt strings.Builder - prompt.WriteString("Provide a concise summary of this conversation, preserving key context:\n") + prompt.WriteString(compactionSummaryPrompt) if len(mediaKinds) > 0 { // Deduplicate and count media types for a compact note. counts := make(map[string]int) for _, k := range mediaKinds { counts[k]++ } - prompt.WriteString("\nNote: user shared media files (") + prompt.WriteString("Note: user shared media files (") first := true for k, n := range counts { if !first { @@ -633,12 +633,12 @@ func (l *Loop) maybeSummarize(ctx context.Context, sessionKey string) { prompt.WriteString(fmt.Sprintf("%d %s(s)", n, k)) first = false } - prompt.WriteString(") which are no longer in context. Mention briefly if relevant.\n") + prompt.WriteString(") which are no longer in context. Mention briefly if relevant.\n\n") } if summary != "" { - prompt.WriteString("Existing context: " + summary + "\n") + prompt.WriteString("Existing context: " + summary + "\n\n") } - prompt.WriteString("\n" + sb.String()) + prompt.WriteString(sb.String()) resp, err := l.provider.Chat(sctx, providers.ChatRequest{ Messages: []providers.Message{{Role: "user", Content: prompt.String()}},