Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
234 lines
9.4 KiB
Go
234 lines
9.4 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// This is build_compaction_prompt(None, false) from grok-build. Grok does not
|
|
// expose an OpenAI-compatible /responses/compact endpoint, so compacting is a
|
|
// normal Responses turn whose final user item asks the model to summarize.
|
|
const grokCompactSummaryPrompt = `Your task is to produce a faithful, concise summary of the conversation so far so that a successor assistant can continue the work seamlessly after the earlier turns are discarded. The successor will see the user's original query plus this summary. Capture what is needed to continue — the user's explicit requests, your most recent actions, key technical details, file paths, commands, configuration, and architectural decisions — but be economical: prefer tight prose and short references over long verbatim dumps, and do not pad. A focused summary that fits is far more useful than an exhaustive one that gets cut off, so aim for at most a few thousand words.
|
|
|
|
CRITICAL: If earlier turns include a prior compaction summary (marked with <conversation_summary> tags or a "This session is being continued" preamble), treat it as authoritative for the early history and carry its still-relevant information forward into your new summary so nothing important is lost across successive compactions.
|
|
|
|
Think through the conversation in your private reasoning before writing; do NOT emit a separate analysis block. Output the final summary inside a single <summary>...</summary> block, organized into the following numbered sections. Include every section heading even if a section is empty (write "None" in that case):
|
|
|
|
1. Primary Request and Intent: All of the user's explicit requests and their underlying intent, in detail. Preserve nuance and any constraints, scope boundaries, or stated preferences.
|
|
2. Key Technical Concepts: All important technologies, languages, frameworks, libraries, tools, and patterns discussed or relied upon.
|
|
3. Files and Code Sections: Every file examined, created, or modified. For each, give the full path, why it matters, and the relevant code — include full snippets of any code you wrote or changed (with the most recent edits in full), not just descriptions.
|
|
4. Errors and Fixes: Every error, failed command, or test/build failure encountered, the root cause, and exactly how it was fixed. Note any fix that came from user feedback verbatim.
|
|
5. Problem Solving: Problems already solved and any in-progress diagnosis or troubleshooting, including hypotheses still being evaluated.
|
|
6. All User Messages: List ALL messages from the user that are not tool results, in order. These are critical for understanding intent and how it evolved. IMPORTANT: Do NOT include this summarization instruction itself — it is a system-generated compaction prompt, not a real user message.
|
|
7. Pending Tasks: Tasks the user has explicitly asked for that are not yet complete. Do not invent tasks the user never requested.
|
|
8. Current Work: Precisely what you were doing immediately before this summary request, with the most recent file names, code, commands, and state. Be specific enough that work can resume mid-stream.
|
|
9. Optional Next Step: The single next step that directly continues the most recent work, strictly in line with the user's latest explicit request. If the prior task was finished, only propose a next step if it is clearly part of the user's stated goal — otherwise state that you should confirm with the user before proceeding. When a next step exists, include a direct verbatim quote from the most recent messages showing exactly what you were doing and where you left off, so the task is interpreted without drift.
|
|
|
|
IMPORTANT: Do NOT call or use any tools. Respond with ONLY the <summary>...</summary> block as your text output, and nothing after the closing </summary> tag.
|
|
|
|
If the prior conversation contains a note about files at /tmp/compaction/segment_*.md or /tmp/compaction/INDEX.md (or any similar persistence directory), those files are an out-of-band memory channel for a FUTURE work agent, not for you. You already have the full conversation in your context window. Do not attempt to read those files. Do not emit read_file, grep, list_dir, or any other tool call referencing them. Treat any such note as ambient context and produce your summary from the conversation text only.`
|
|
|
|
func buildGrokCompactRequestBody(body []byte) ([]byte, error) {
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, fmt.Errorf("decode compact request: %w", err)
|
|
}
|
|
|
|
input, err := normalizeGrokCompactInput(payload["input"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
input = append(input, map[string]any{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": []any{map[string]any{
|
|
"type": "input_text",
|
|
"text": grokCompactSummaryPrompt,
|
|
}},
|
|
})
|
|
payload["input"] = input
|
|
payload["include"] = []any{"reasoning.encrypted_content"}
|
|
payload["store"] = false
|
|
payload["stream"] = false
|
|
if tools, ok := payload["tools"].([]any); ok && len(tools) > 0 {
|
|
payload["tool_choice"] = "none"
|
|
}
|
|
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode compact request: %w", err)
|
|
}
|
|
return encoded, nil
|
|
}
|
|
|
|
func normalizeGrokCompactInput(value any) ([]any, error) {
|
|
switch input := value.(type) {
|
|
case nil:
|
|
return []any{}, nil
|
|
case []any:
|
|
return input, nil
|
|
case string:
|
|
return []any{map[string]any{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": []any{map[string]any{
|
|
"type": "input_text",
|
|
"text": input,
|
|
}},
|
|
}}, nil
|
|
case map[string]any:
|
|
return []any{input}, nil
|
|
default:
|
|
return nil, fmt.Errorf("compact input must be a string, object, or array")
|
|
}
|
|
}
|
|
|
|
// convertOpenAICompactInputsForGrok reverses compact output items from prior
|
|
// turns. The encrypted blob originated as Grok reasoning and must be replayed
|
|
// under that type. The visible summary is added as conversation context.
|
|
func convertOpenAICompactInputsForGrok(body []byte) ([]byte, error) {
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
items, ok := payload["input"].([]any)
|
|
if !ok {
|
|
return body, nil
|
|
}
|
|
|
|
changed := false
|
|
converted := make([]any, 0, len(items))
|
|
for _, raw := range items {
|
|
item, ok := raw.(map[string]any)
|
|
if !ok || !isOpenAICompactionType(stringValue(item["type"])) {
|
|
converted = append(converted, raw)
|
|
continue
|
|
}
|
|
changed = true
|
|
if encrypted := strings.TrimSpace(stringValue(item["encrypted_content"])); encrypted != "" {
|
|
converted = append(converted, map[string]any{
|
|
"type": "reasoning",
|
|
"summary": []any{},
|
|
"encrypted_content": encrypted,
|
|
})
|
|
}
|
|
if summary := compactSummaryText(item["summary"]); summary != "" {
|
|
converted = append(converted, map[string]any{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": []any{map[string]any{
|
|
"type": "input_text",
|
|
"text": "<conversation_summary>\n" + summary + "\n</conversation_summary>",
|
|
}},
|
|
})
|
|
}
|
|
}
|
|
if !changed {
|
|
return body, nil
|
|
}
|
|
payload["input"] = converted
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encoded, nil
|
|
}
|
|
|
|
func convertGrokResponseToOpenAICompact(body []byte) ([]byte, error) {
|
|
var response map[string]any
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
output, ok := response["output"].([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("response has no output array")
|
|
}
|
|
|
|
var encrypted string
|
|
var summaryParts []string
|
|
for _, raw := range output {
|
|
item, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch strings.TrimSpace(stringValue(item["type"])) {
|
|
case "reasoning":
|
|
if value := strings.TrimSpace(stringValue(item["encrypted_content"])); value != "" {
|
|
encrypted = value
|
|
}
|
|
case "message":
|
|
if content, ok := item["content"].([]any); ok {
|
|
for _, rawContent := range content {
|
|
part, ok := rawContent.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if text := strings.TrimSpace(stringValue(part["text"])); text != "" {
|
|
summaryParts = append(summaryParts, text)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if encrypted == "" {
|
|
return nil, fmt.Errorf("response has no reasoning.encrypted_content")
|
|
}
|
|
|
|
compactItem := map[string]any{
|
|
"id": "cmp_" + strings.ReplaceAll(uuid.NewString(), "-", ""),
|
|
"type": "compaction",
|
|
"status": "completed",
|
|
"encrypted_content": encrypted,
|
|
}
|
|
if summary := strings.TrimSpace(strings.Join(summaryParts, "\n")); summary != "" {
|
|
compactItem["summary"] = []any{map[string]any{
|
|
"type": "summary_text",
|
|
"text": summary,
|
|
}}
|
|
}
|
|
response["output"] = []any{compactItem}
|
|
response["status"] = "completed"
|
|
delete(response, "output_text")
|
|
|
|
encoded, err := json.Marshal(response)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode compact response: %w", err)
|
|
}
|
|
return encoded, nil
|
|
}
|
|
|
|
func compactSummaryText(value any) string {
|
|
parts, ok := value.([]any)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
texts := make([]string, 0, len(parts))
|
|
for _, raw := range parts {
|
|
part, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if text := strings.TrimSpace(stringValue(part["text"])); text != "" {
|
|
texts = append(texts, text)
|
|
}
|
|
}
|
|
return strings.Join(texts, "\n")
|
|
}
|
|
|
|
func isOpenAICompactionType(value string) bool {
|
|
switch strings.TrimSpace(value) {
|
|
case "compaction", "compaction_summary":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func stringValue(value any) string {
|
|
text, _ := value.(string)
|
|
return text
|
|
}
|