package server import ( "encoding/json" "fmt" "regexp" "strings" "github.com/daniel/cursor-adapter/internal/sanitize" "github.com/daniel/cursor-adapter/internal/types" ) // systemReminderRe matches ... blocks // that Claude Desktop embeds inside user messages. var systemReminderRe = regexp.MustCompile(`(?s).*?\s*`) // buildPromptFromAnthropicMessages flattens an Anthropic Messages request // into a single prompt string suitable for `agent --print`. // // "Pure brain + remote executors" design: // - DROP all client system messages (mode descriptions / sandbox warnings // that make the model refuse). // - USE ONLY the adapter's injected system prompt. // - RENDER req.Tools as a plain-text inventory of executors that the // caller (Claude Desktop / Claude Code / opencode) owns. The brain must // know it has remote hands. // - RENDER assistant tool_use and user tool_result blocks as readable // transcript, so multi-turn ReAct loops keep working. // - STRIP blocks embedded in user messages. func buildPromptFromAnthropicMessages(req types.AnthropicMessagesRequest, injectedSystemPrompt string) string { var prompt strings.Builder if injectedSystemPrompt != "" { prompt.WriteString("System:\n") prompt.WriteString(injectedSystemPrompt) prompt.WriteString("\n\n") } if hints := renderMountHints(extractMountHints(req)); hints != "" { prompt.WriteString(hints) prompt.WriteString("\n") } if toolsBlock := renderToolsForBrain(req.Tools); toolsBlock != "" { prompt.WriteString(toolsBlock) prompt.WriteString("\n") } for _, msg := range req.Messages { text := renderMessageBlocks(msg.Role, msg.Content) if text == "" { continue } switch msg.Role { case "assistant": prompt.WriteString("Assistant: ") default: prompt.WriteString("User: ") } prompt.WriteString(text) prompt.WriteString("\n\n") } prompt.WriteString("Assistant:") return prompt.String() } // renderToolsForBrain converts the Anthropic tools[] array into a readable // inventory the brain can reason about. The brain is told it MUST emit // {...} sentinels when it wants to invoke one; the // proxy translates that into real Anthropic tool_use blocks for the caller. func renderToolsForBrain(tools []types.AnthropicTool) string { if len(tools) == 0 { return "" } var b strings.Builder b.WriteString("Available executors (the caller will run these for you):\n") for _, t := range tools { b.WriteString("- ") b.WriteString(t.Name) if desc := strings.TrimSpace(t.Description); desc != "" { b.WriteString(": ") b.WriteString(singleLine(desc)) } if len(t.InputSchema) > 0 { b.WriteString("\n input_schema: ") b.WriteString(compactJSON(t.InputSchema)) } b.WriteString("\n") } b.WriteString("\nTo invoke a tool, output EXACTLY one fenced block (and nothing else for that turn):\n") b.WriteString("\n") b.WriteString(`{"name":"","input":{...}}` + "\n") b.WriteString("\n") b.WriteString("If you do NOT need a tool, just answer in plain text.\n") return b.String() } // renderMessageBlocks renders a single message's content blocks into a // transcript snippet. Text blocks are sanitised; tool_use blocks render as // `[tool_call name=... input=...]`; tool_result blocks render as // `[tool_result for=... ok|error] ...`. func renderMessageBlocks(role string, content types.AnthropicContent) string { var parts []string for _, block := range content { switch block.Type { case "text": if block.Text == "" { continue } cleaned := systemReminderRe.ReplaceAllString(block.Text, "") cleaned = sanitize.Text(cleaned) cleaned = strings.TrimSpace(cleaned) if cleaned != "" { parts = append(parts, cleaned) } case "tool_use": parts = append(parts, fmt.Sprintf( "[tool_call name=%q input=%s]", block.Name, compactJSON(block.Input), )) case "tool_result": status := "ok" if block.IsError { status = "error" } body := renderToolResultContent(block.Content) if body == "" { body = "(empty)" } parts = append(parts, fmt.Sprintf( "[tool_result for=%s status=%s]\n%s", block.ToolUseID, status, body, )) case "image", "document": parts = append(parts, fmt.Sprintf("[%s attached]", block.Type)) } } return strings.Join(parts, "\n") } // renderToolResultContent flattens a tool_result.content payload (which can // be a string or an array of {type:"text",text:...} blocks) to plain text. func renderToolResultContent(raw json.RawMessage) string { if len(raw) == 0 { return "" } var s string if err := json.Unmarshal(raw, &s); err == nil { return strings.TrimSpace(s) } var blocks []struct { Type string `json:"type"` Text string `json:"text"` } if err := json.Unmarshal(raw, &blocks); err == nil { var out []string for _, b := range blocks { if b.Type == "text" && b.Text != "" { out = append(out, b.Text) } } return strings.TrimSpace(strings.Join(out, "\n")) } return strings.TrimSpace(string(raw)) } func compactJSON(raw json.RawMessage) string { if len(raw) == 0 { return "{}" } var v interface{} if err := json.Unmarshal(raw, &v); err != nil { return string(raw) } out, err := json.Marshal(v) if err != nil { return string(raw) } return string(out) } func singleLine(s string) string { s = strings.ReplaceAll(s, "\r", " ") s = strings.ReplaceAll(s, "\n", " ") for strings.Contains(s, " ") { s = strings.ReplaceAll(s, " ", " ") } return strings.TrimSpace(s) }